Skip to content

Commit 308100d

Browse files
authored
fix: Implement anonymous context processing (#350)
Apologies, I missed implementing this when moving the project to js-core.
1 parent 2c6add5 commit 308100d

File tree

11 files changed

+302
-63
lines changed

11 files changed

+302
-63
lines changed

packages/shared/common/src/Context.ts

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
/* eslint-disable no-underscore-dangle */
22
// eslint-disable-next-line max-classes-per-file
3-
import { LDContextCommon, LDMultiKindContext, LDSingleKindContext, LDUser } from './api/context';
4-
import { LDContext } from './api/context/LDContext';
3+
import type {
4+
LDContext,
5+
LDContextCommon,
6+
LDMultiKindContext,
7+
LDSingleKindContext,
8+
LDUser,
9+
} from './api';
510
import AttributeReference from './AttributeReference';
11+
import { isLegacyUser, isMultiKind, isSingleKind } from './internal/context';
612
import { TypeValidators } from './validators';
713

814
// The general strategy for the context is to transform the passed in context
@@ -38,39 +44,6 @@ function encodeKey(key: string): string {
3844
return key;
3945
}
4046

41-
/**
42-
* Check if a context is a single kind context.
43-
* @param context
44-
* @returns true if the context is a single kind context.
45-
*/
46-
function isSingleKind(context: LDContext): context is LDSingleKindContext {
47-
if ('kind' in context) {
48-
return TypeValidators.String.is(context.kind) && context.kind !== 'multi';
49-
}
50-
return false;
51-
}
52-
53-
/**
54-
* Check if a context is a multi-kind context.
55-
* @param context
56-
* @returns true if it is a multi-kind context.
57-
*/
58-
function isMultiKind(context: LDContext): context is LDMultiKindContext {
59-
if ('kind' in context) {
60-
return TypeValidators.String.is(context.kind) && context.kind === 'multi';
61-
}
62-
return false;
63-
}
64-
65-
/**
66-
* Check if a context is a legacy user context.
67-
* @param context
68-
* @returns true if it is a legacy user context.
69-
*/
70-
function isLegacyUser(context: LDContext): context is LDUser {
71-
return !('kind' in context) || context.kind === null || context.kind === undefined;
72-
}
73-
7447
/**
7548
* Check if the given value is a LDContextCommon.
7649
* @param kindOrContext
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Internal use only. These functions should only be used as part of the initial validation of
3+
* the LDContext object. Thereafter, the Context object should be used.
4+
*/
5+
import type { LDContext, LDMultiKindContext, LDSingleKindContext, LDUser } from '../../api';
6+
import { TypeValidators } from '../../validators';
7+
8+
/**
9+
* Check if a context is a single kind context.
10+
* @param context
11+
* @returns true if the context is a single kind context.
12+
*/
13+
export function isSingleKind(context: LDContext): context is LDSingleKindContext {
14+
if ('kind' in context) {
15+
return TypeValidators.String.is(context.kind) && context.kind !== 'multi';
16+
}
17+
return false;
18+
}
19+
20+
/**
21+
* Check if a context is a multi-kind context.
22+
* @param context
23+
* @returns true if it is a multi-kind context.
24+
*/
25+
export function isMultiKind(context: LDContext): context is LDMultiKindContext {
26+
if ('kind' in context) {
27+
return TypeValidators.String.is(context.kind) && context.kind === 'multi';
28+
}
29+
return false;
30+
}
31+
32+
/**
33+
* Check if a context is a legacy user context.
34+
* @param context
35+
* @returns true if it is a legacy user context.
36+
*/
37+
export function isLegacyUser(context: LDContext): context is LDUser {
38+
return !('kind' in context) || context.kind === null || context.kind === undefined;
39+
}

packages/shared/common/src/internal/diagnostics/DiagnosticsManager.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ describe('given a diagnostics manager', () => {
1616
});
1717

1818
beforeEach(() => {
19+
basicPlatform.crypto.randomUUID.mockReturnValueOnce('random1').mockReturnValueOnce('random2');
1920
manager = new DiagnosticsManager('my-sdk-key', basicPlatform, { test1: 'value1' });
2021
});
2122

packages/shared/common/src/internal/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './diagnostics';
22
export * from './evaluation';
33
export * from './events';
44
export * from './stream';
5+
export * from './context';

packages/shared/mocks/src/hasher.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ export const crypto: Crypto = {
1616
// Not used for this test.
1717
throw new Error(`Function not implemented.${algorithm}${key}`);
1818
},
19-
randomUUID(): string {
19+
randomUUID: jest.fn(() => {
2020
counter += 1;
2121
// Will provide a unique value for tests.
2222
// Very much not a UUID of course.
2323
return `${counter}`;
24-
},
24+
}),
2525
};

packages/shared/sdk-client/src/LDClientImpl.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ jest.mock('@launchdarkly/js-sdk-common', () => {
1919
};
2020
});
2121

22+
const { crypto } = basicPlatform;
2223
const testSdkKey = 'test-sdk-key';
2324
const context: LDContext = { kind: 'org', key: 'Testy Pizza' };
2425
let ldc: LDClientImpl;
@@ -28,6 +29,7 @@ describe('sdk-client object', () => {
2829
beforeEach(() => {
2930
defaultPutResponse = clone<Flags>(mockResponseJson);
3031
setupMockStreamingProcessor(false, defaultPutResponse);
32+
crypto.randomUUID.mockReturnValueOnce('random1');
3133

3234
ldc = new LDClientImpl(testSdkKey, basicPlatform, { logger, sendEvents: false });
3335
jest
@@ -133,6 +135,20 @@ describe('sdk-client object', () => {
133135
});
134136
});
135137

138+
test('identify anonymous', async () => {
139+
defaultPutResponse['dev-test-flag'].value = false;
140+
const carContext: LDContext = { kind: 'car', anonymous: true, key: '' };
141+
142+
await ldc.identify(carContext);
143+
const c = ldc.getContext();
144+
const all = ldc.allFlags();
145+
146+
expect(c!.key).toEqual('random1');
147+
expect(all).toMatchObject({
148+
'dev-test-flag': false,
149+
});
150+
});
151+
136152
test('identify error invalid context', async () => {
137153
// @ts-ignore
138154
const carContext: LDContext = { kind: 'car', key: undefined };

packages/shared/sdk-client/src/LDClientImpl.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import createDiagnosticsManager from './diagnostics/createDiagnosticsManager';
2424
import createEventProcessor from './events/createEventProcessor';
2525
import EventFactory from './events/EventFactory';
2626
import { DeleteFlag, Flags, PatchFlag } from './types';
27-
import { calculateFlagChanges } from './utils';
27+
import { calculateFlagChanges, ensureKey } from './utils';
2828

2929
const { createErrorEvaluationDetail, createSuccessEvaluationDetail, ClientMessages, ErrorKinds } =
3030
internal;
@@ -231,7 +231,9 @@ export default class LDClientImpl implements LDClient {
231231
}
232232

233233
// TODO: implement secure mode
234-
async identify(context: LDContext, _hash?: string): Promise<void> {
234+
async identify(pristineContext: LDContext, _hash?: string): Promise<void> {
235+
const context = await ensureKey(pristineContext, this.platform);
236+
235237
const checkedContext = Context.fromLDContext(context);
236238
if (!checkedContext.valid) {
237239
const error = new Error('Context was unspecified or had no key');
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { fastDeepEqual } from '@launchdarkly/js-sdk-common';
2+
3+
import { Flags } from '../types';
4+
5+
// eslint-disable-next-line import/prefer-default-export
6+
export default function calculateFlagChanges(flags: Flags, incomingFlags: Flags) {
7+
const changedKeys: string[] = [];
8+
9+
// flag deleted or updated
10+
Object.entries(flags).forEach(([k, f]) => {
11+
const incoming = incomingFlags[k];
12+
if (!incoming || !fastDeepEqual(f, incoming)) {
13+
changedKeys.push(k);
14+
}
15+
});
16+
17+
// flag added
18+
Object.keys(incomingFlags).forEach((k) => {
19+
if (!flags[k]) {
20+
changedKeys.push(k);
21+
}
22+
});
23+
24+
return changedKeys;
25+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import type {
2+
LDContext,
3+
LDContextCommon,
4+
LDMultiKindContext,
5+
LDUser,
6+
} from '@launchdarkly/js-sdk-common';
7+
import { basicPlatform } from '@launchdarkly/private-js-mocks';
8+
9+
import ensureKey, { addNamespace, getOrGenerateKey } from './ensureKey';
10+
11+
const { crypto, storage } = basicPlatform;
12+
describe('ensureKey', () => {
13+
beforeEach(() => {
14+
crypto.randomUUID.mockReturnValueOnce('random1').mockReturnValueOnce('random2');
15+
});
16+
17+
afterEach(() => {
18+
jest.resetAllMocks();
19+
});
20+
21+
test('addNamespace', async () => {
22+
const nsKey = addNamespace('org');
23+
expect(nsKey).toEqual('LaunchDarkly_AnonKeys_org');
24+
});
25+
26+
test('getOrGenerateKey create new key', async () => {
27+
const key = await getOrGenerateKey('org', basicPlatform);
28+
29+
expect(key).toEqual('random1');
30+
expect(crypto.randomUUID).toHaveBeenCalled();
31+
expect(storage.get).toHaveBeenCalledWith('LaunchDarkly_AnonKeys_org');
32+
expect(storage.set).toHaveBeenCalledWith('LaunchDarkly_AnonKeys_org', 'random1');
33+
});
34+
35+
test('getOrGenerateKey existing key', async () => {
36+
storage.get.mockImplementation((nsKind: string) =>
37+
nsKind === 'LaunchDarkly_AnonKeys_org' ? 'random1' : undefined,
38+
);
39+
40+
const key = await getOrGenerateKey('org', basicPlatform);
41+
42+
expect(key).toEqual('random1');
43+
expect(crypto.randomUUID).not.toHaveBeenCalled();
44+
expect(storage.get).toHaveBeenCalledWith('LaunchDarkly_AnonKeys_org');
45+
expect(storage.set).not.toHaveBeenCalled();
46+
});
47+
48+
test('ensureKey should not override anonymous key if specified', async () => {
49+
const context: LDContext = { kind: 'org', anonymous: true, key: 'Testy Pizza' };
50+
const c = await ensureKey(context, basicPlatform);
51+
52+
expect(c.key).toEqual('Testy Pizza');
53+
});
54+
55+
test('ensureKey non-anonymous single context should be unchanged', async () => {
56+
const context: LDContext = { kind: 'org', key: 'Testy Pizza' };
57+
const c = await ensureKey(context, basicPlatform);
58+
59+
expect(c.key).toEqual('Testy Pizza');
60+
expect(c.anonymous).toBeFalsy();
61+
});
62+
63+
test('ensureKey non-anonymous contexts in multi should be unchanged', async () => {
64+
const context: LDContext = {
65+
kind: 'multi',
66+
user: { key: 'userKey' },
67+
org: { key: 'orgKey' },
68+
};
69+
70+
const c = (await ensureKey(context, basicPlatform)) as LDMultiKindContext;
71+
72+
expect((c.user as LDContextCommon).key).toEqual('userKey');
73+
expect((c.org as LDContextCommon).key).toEqual('orgKey');
74+
});
75+
76+
test('ensureKey should create key for single anonymous context', async () => {
77+
const context: LDContext = { kind: 'org', anonymous: true, key: '' };
78+
const c = await ensureKey(context, basicPlatform);
79+
expect(c.key).toEqual('random1');
80+
});
81+
82+
test('ensureKey should create key for an anonymous context in multi', async () => {
83+
const context: LDContext = {
84+
kind: 'multi',
85+
user: { anonymous: true, key: '' },
86+
org: { key: 'orgKey' },
87+
};
88+
89+
const c = (await ensureKey(context, basicPlatform)) as LDMultiKindContext;
90+
91+
expect((c.user as LDContextCommon).key).toEqual('random1');
92+
expect((c.org as LDContextCommon).key).toEqual('orgKey');
93+
});
94+
95+
test('ensureKey should create key for all anonymous contexts in multi', async () => {
96+
const context: LDContext = {
97+
kind: 'multi',
98+
user: { anonymous: true, key: '' },
99+
org: { anonymous: true, key: '' },
100+
};
101+
102+
const c = (await ensureKey(context, basicPlatform)) as LDMultiKindContext;
103+
104+
expect((c.user as LDContextCommon).key).toEqual('random1');
105+
expect((c.org as LDContextCommon).key).toEqual('random2');
106+
});
107+
108+
test('ensureKey should create key for anonymous legacy user', async () => {
109+
const context: LDUser = {
110+
anonymous: true,
111+
key: '',
112+
};
113+
const c = await ensureKey(context, basicPlatform);
114+
expect(c.key).toEqual('random1');
115+
});
116+
});

0 commit comments

Comments
 (0)