Skip to content

Commit da42162

Browse files
feat: Add ODP/ATS VuidManager (#776)
* Add VuidManager tests and empty class file * Move tests location, Typescript, & jest * Initial implementation VuidManager (1 test WIP) * Add persistence cache * Implement cache into VuidManager * Correct persistentKeyValueCache's types * Update persistent cache func descriptions * Adjust VuidManager tests * Add React Native and browser persistence classes * Add React Native and browser persistence classes * Testing for ReactNativeAsyncStorageCache * More testing for ReactNativeAsyncStorageCache * Update to a getter instance() * BrowserAsyncStorageCache tests * Bug fix VuidManager and tests * Add @react-native-async-storage/async-storage * Update packages/optimizely-sdk/lib/plugins/vuid_manager/index.ts Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com> * Add test around and correct load() * Delete inMemoryAsyncStorageCache * Add dev packages react-native-async-storage & jest-ts-auto-mock * Refactor to use async/await + simplifying returns * Change to ts-mockito for stubs/mocks * Add _reset() for testing * Refactor VUID Manager tests using ts-mockito * Un-nest vuid stored in cache * Add JSDoc * Corrections to accessibility and interface * Corrections to tests * Update to package-lock * Fix to AsyncStorageCache types and tests Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com>
1 parent 625cb7c commit da42162

File tree

9 files changed

+27879
-13843
lines changed

9 files changed

+27879
-13843
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Copyright 2022, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import PersistentKeyValueCache from './persistentKeyValueCache';
18+
19+
export default class BrowserAsyncStorageCache implements PersistentKeyValueCache {
20+
async contains(key: string): Promise<boolean> {
21+
return localStorage.getItem(key) !== null;
22+
}
23+
24+
async get(key: string): Promise<string | null> {
25+
return localStorage.getItem(key);
26+
}
27+
28+
async remove(key: string): Promise<boolean> {
29+
if (await this.contains(key)) {
30+
localStorage.removeItem(key);
31+
return true;
32+
} else {
33+
return false;
34+
}
35+
}
36+
37+
async set(key: string, val: string): Promise<void> {
38+
return localStorage.setItem(key, val);
39+
}
40+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Copyright 2022, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* An Interface to implement a persistent key value cache which supports strings as keys and value objects.
19+
*/
20+
export default interface PersistentKeyValueCache {
21+
/**
22+
* Checks if a key exists in the cache
23+
* @param key
24+
* Resolves promise with
25+
* 1. true if the key exists
26+
* 2. false if the key does not exist
27+
* Rejects the promise in case of an error
28+
*/
29+
contains(key: string): Promise<boolean>;
30+
31+
/**
32+
* Returns value stored against a key or undefined if not found.
33+
* @param key
34+
* @returns
35+
* Resolves promise with
36+
* 1. object as value if found.
37+
* 2. undefined if the key does not exist in the cache.
38+
* Rejects the promise in case of an error
39+
*/
40+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
41+
get(key: string): Promise<any>;
42+
43+
/**
44+
* Removes the key value pair from cache.
45+
* @param key *
46+
* @returns
47+
* Resolves promise with
48+
* 1. true if key-value was removed or
49+
* 2. false if key not found
50+
* Rejects the promise in case of an error
51+
*/
52+
remove(key: string): Promise<boolean>;
53+
54+
/**
55+
* Stores any object in the persistent cache against a key
56+
* @param key
57+
* @param val
58+
* @returns
59+
* Resolves promise without a value if successful
60+
* Rejects the promise in case of an error
61+
*/
62+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
63+
set(key: string, val: any): Promise<void>;
64+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Copyright 2020, 2022, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import AsyncStorage from '@react-native-async-storage/async-storage';
18+
import PersistentKeyValueCache from './persistentKeyValueCache';
19+
20+
export default class ReactNativeAsyncStorageCache implements PersistentKeyValueCache {
21+
async contains(key: string): Promise<boolean> {
22+
return await AsyncStorage.getItem(key) !== null;
23+
}
24+
25+
async get(key: string): Promise<string | null> {
26+
return await AsyncStorage.getItem(key);
27+
}
28+
29+
async remove(key: string): Promise<boolean> {
30+
if (await this.contains(key)) {
31+
await AsyncStorage.removeItem(key);
32+
return true;
33+
}
34+
return false;
35+
}
36+
37+
set(key: string, val: string): Promise<void> {
38+
return AsyncStorage.setItem(key, val);
39+
}
40+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
* Copyright 2022, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { uuid } from '../../utils/fns';
18+
import PersistentKeyValueCache from '../key_value_cache/persistentKeyValueCache';
19+
20+
export interface IVuidManager {
21+
readonly vuid: string;
22+
}
23+
24+
/**
25+
* Manager for creating, persisting, and retrieving a Visitor Unique Identifier
26+
*/
27+
export class VuidManager implements IVuidManager {
28+
/**
29+
* Unique key used within the persistent value cache against which to
30+
* store the VUID
31+
* @private
32+
*/
33+
private _keyForVuid = 'optimizely-vuid';
34+
35+
/**
36+
* Prefix used as part of the VUID format
37+
* @private
38+
*/
39+
private readonly _prefix: string = `vuid_`;
40+
41+
/**
42+
* Current VUID value being used
43+
* @private
44+
*/
45+
private _vuid: string;
46+
47+
/**
48+
* Get the current VUID value being used
49+
*/
50+
public get vuid(): string {
51+
return this._vuid;
52+
}
53+
54+
private constructor() {
55+
this._vuid = '';
56+
}
57+
58+
/**
59+
* Instance of the VUID Manager
60+
* @private
61+
*/
62+
private static _instance: VuidManager;
63+
64+
/**
65+
* Gets the current instance of the VUID Manager, initializing if needed
66+
* @param cache Caching mechanism to use for persisting the VUID outside working memory *
67+
* @returns An instance of VuidManager
68+
*/
69+
public static async instance(cache: PersistentKeyValueCache): Promise<VuidManager> {
70+
if (!this._instance) {
71+
this._instance = new VuidManager();
72+
}
73+
74+
if (!this._instance._vuid) {
75+
await this._instance.load(cache);
76+
}
77+
78+
return this._instance;
79+
}
80+
81+
/**
82+
* Attempts to load a VUID from persistent cache or generates a new VUID
83+
* @param cache Caching mechanism to use for persisting the VUID outside working memory
84+
* @returns Current VUID stored in the VuidManager
85+
*/
86+
private async load(cache: PersistentKeyValueCache): Promise<string> {
87+
const cachedValue = await cache.get(this._keyForVuid);
88+
if (cachedValue && this.isVuid(cachedValue)) {
89+
this._vuid = cachedValue;
90+
} else {
91+
this._vuid = this.makeVuid();
92+
await this.save(this._vuid, cache);
93+
}
94+
95+
return this._vuid;
96+
}
97+
98+
/**
99+
* Creates a new VUID
100+
* @returns A new visitor unique identifier
101+
*/
102+
private makeVuid(): string {
103+
const maxLength = 32; // required by ODP server
104+
105+
// make sure UUIDv4 is used (not UUIDv1 or UUIDv6) since the trailing 5 chars will be truncated. See TDD for details.
106+
const uuidV4 = uuid();
107+
const formatted = uuidV4.replace(/-/g, '').toLowerCase();
108+
const vuidFull = `${(this._prefix)}${formatted}`;
109+
110+
return (vuidFull.length <= maxLength) ? vuidFull : vuidFull.substring(0, maxLength);
111+
}
112+
113+
/**
114+
* Saves a VUID to a persistent cache
115+
* @param vuid VUID to be stored
116+
* @param cache Caching mechanism to use for persisting the VUID outside working memory
117+
*/
118+
private async save(vuid: string, cache: PersistentKeyValueCache): Promise<void> {
119+
await cache.set(this._keyForVuid, vuid);
120+
}
121+
122+
/**
123+
* Validates the format of a Visitor Unique Identifier
124+
* @param vuid VistorId to check
125+
* @returns *true* if the VisitorId is valid otherwise *false* for invalid
126+
*/
127+
private isVuid = (vuid: string): boolean => vuid.startsWith(this._prefix);
128+
129+
/**
130+
* Function used in unit testing to reset the VuidManager
131+
* **Important**: This should not to be used in production code
132+
*/
133+
private static _reset(): void {
134+
this._instance._vuid = '';
135+
}
136+
}

0 commit comments

Comments
 (0)