Skip to content

Commit 58406f0

Browse files
authored
Allow configuring a custom event store in React Native tracker (close #1413)
PR #1418
1 parent 40b52d5 commit 58406f0

File tree

9 files changed

+196
-59
lines changed

9 files changed

+196
-59
lines changed

Diff for: trackers/react-native-tracker/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"test": "jest"
4444
},
4545
"peerDependencies": {
46+
"@react-native-async-storage/async-storage": "~2.0.0",
4647
"react": "*",
4748
"react-native": "*",
4849
"react-native-get-random-values": "^1.11.0"
@@ -51,11 +52,11 @@
5152
"@snowplow/tracker-core": "workspace:*",
5253
"@snowplow/browser-tracker-core": "workspace:*",
5354
"@snowplow/browser-plugin-screen-tracking": "workspace:*",
54-
"@react-native-async-storage/async-storage": "~2.0.0",
5555
"tslib": "^2.3.1",
5656
"uuid": "^10.0.0"
5757
},
5858
"devDependencies": {
59+
"@react-native-async-storage/async-storage": "~2.0.0",
5960
"@snowplow/browser-plugin-snowplow-ecommerce": "workspace:*",
6061
"@typescript-eslint/eslint-plugin": "~5.15.0",
6162
"@typescript-eslint/parser": "~5.15.0",

Diff for: trackers/react-native-tracker/src/event_store.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
import { EventStore, newInMemoryEventStore, EventStorePayload } from '@snowplow/tracker-core';
2-
import { EventStoreConfiguration, TrackerConfiguration } from './types';
3-
import AsyncStorage from '@react-native-async-storage/async-storage';
1+
import { EventStore, EventStorePayload, newInMemoryEventStore } from '@snowplow/tracker-core';
2+
import { AsyncStorage, EventStoreConfiguration, TrackerConfiguration } from './types';
3+
4+
type Configuration = Omit<EventStoreConfiguration, 'asyncStorage'> &
5+
TrackerConfiguration & { asyncStorage: AsyncStorage };
46

57
export async function newReactNativeEventStore({
68
namespace,
79
maxEventStoreSize = 1000,
810
useAsyncStorageForEventStore: useAsyncStorage = true,
9-
}: EventStoreConfiguration & TrackerConfiguration): Promise<EventStore> {
11+
asyncStorage,
12+
}: Configuration): Promise<EventStore> {
1013
const queueName = `snowplow_${namespace}`;
1114

1215
async function newInMemoryEventStoreForReactNative() {
1316
if (useAsyncStorage) {
14-
const data = await AsyncStorage.getItem(queueName);
17+
const data = await asyncStorage.getItem(queueName);
1518
const events: EventStorePayload[] = data ? JSON.parse(data) : [];
1619
return newInMemoryEventStore({ maxSize: maxEventStoreSize, events });
1720
} else {
@@ -26,7 +29,7 @@ export async function newReactNativeEventStore({
2629
async function sync() {
2730
if (useAsyncStorage) {
2831
const events = await getAll();
29-
await AsyncStorage.setItem(queueName, JSON.stringify(events));
32+
await asyncStorage.setItem(queueName, JSON.stringify(events));
3033
}
3134
}
3235

Diff for: trackers/react-native-tracker/src/plugins/app_install/index.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { buildSelfDescribingEvent, CorePluginConfiguration, TrackerCore } from '@snowplow/tracker-core';
2-
import { AppLifecycleConfiguration, TrackerConfiguration } from '../../types';
1+
import type { CorePluginConfiguration, TrackerCore } from '@snowplow/tracker-core';
2+
import { buildSelfDescribingEvent } from '@snowplow/tracker-core';
33
import { APPLICATION_INSTALL_EVENT_SCHEMA } from '../../constants';
4-
import AsyncStorage from '@react-native-async-storage/async-storage';
4+
import type { AppLifecycleConfiguration, AsyncStorage, TrackerConfiguration } from '../../types';
55

66
/**
77
* Tracks an application install event on the first run of the app.
@@ -10,14 +10,18 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
1010
* Event schema: `iglu:com.snowplowanalytics.mobile/application_install/jsonschema/1-0-0`
1111
*/
1212
export function newAppInstallPlugin(
13-
{ namespace, installAutotracking = false }: TrackerConfiguration & AppLifecycleConfiguration,
13+
{
14+
asyncStorage,
15+
namespace,
16+
installAutotracking = false,
17+
}: TrackerConfiguration & AppLifecycleConfiguration & { asyncStorage: AsyncStorage },
1418
core: TrackerCore
1519
): CorePluginConfiguration {
1620
if (installAutotracking) {
1721
// Track install event on first run
1822
const key = `snowplow_${namespace}_install`;
1923
setTimeout(async () => {
20-
const installEvent = await AsyncStorage.getItem(key);
24+
const installEvent = await asyncStorage.getItem(key);
2125
if (!installEvent) {
2226
core.track(
2327
buildSelfDescribingEvent({
@@ -27,7 +31,7 @@ export function newAppInstallPlugin(
2731
},
2832
})
2933
);
30-
await AsyncStorage.setItem(key, new Date().toISOString());
34+
await asyncStorage.setItem(key, new Date().toISOString());
3135
}
3236
}, 0);
3337
}

Diff for: trackers/react-native-tracker/src/plugins/session/index.ts

+12-12
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { CorePluginConfiguration, PayloadBuilder } from '@snowplow/tracker-core';
2-
import { SessionConfiguration, SessionState, TrackerConfiguration } from '../../types';
3-
import AsyncStorage from '@react-native-async-storage/async-storage';
42
import { v4 as uuidv4 } from 'uuid';
53
import { BACKGROUND_EVENT_SCHEMA, CLIENT_SESSION_ENTITY_SCHEMA, FOREGROUND_EVENT_SCHEMA } from '../../constants';
4+
import { AsyncStorage, SessionConfiguration, SessionState, TrackerConfiguration } from '../../types';
65
import { getUsefulSchema } from '../../utils';
76

87
interface StoredSessionState {
@@ -19,13 +18,13 @@ interface SessionPlugin extends CorePluginConfiguration {
1918
startNewSession: () => Promise<void>;
2019
}
2120

22-
async function storeSessionState(namespace: string, state: StoredSessionState) {
21+
async function storeSessionState(namespace: string, state: StoredSessionState, asyncStorage: AsyncStorage) {
2322
const { userId, sessionId, sessionIndex } = state;
24-
await AsyncStorage.setItem(`snowplow_${namespace}_session`, JSON.stringify({ userId, sessionId, sessionIndex }));
23+
await asyncStorage.setItem(`snowplow_${namespace}_session`, JSON.stringify({ userId, sessionId, sessionIndex }));
2524
}
2625

27-
async function resumeStoredSession(namespace: string): Promise<SessionState> {
28-
const storedState = await AsyncStorage.getItem(`snowplow_${namespace}_session`);
26+
async function resumeStoredSession(namespace: string, asyncStorage: AsyncStorage): Promise<SessionState> {
27+
const storedState = await asyncStorage.getItem(`snowplow_${namespace}_session`);
2928
if (storedState) {
3029
const state = JSON.parse(storedState) as StoredSessionState;
3130
return {
@@ -48,18 +47,19 @@ async function resumeStoredSession(namespace: string): Promise<SessionState> {
4847
/**
4948
* Creates a new session plugin for tracking the session information.
5049
* The plugin will add the session context to all events and start a new session if the current one has timed out.
51-
*
52-
* The session state is stored in AsyncStorage.
50+
*
51+
* The session state is stored in the defined application storage.
5352
* Each restart of the app or creation of a new tracker instance will trigger a new session with reference to the previous session.
5453
*/
5554
export async function newSessionPlugin({
55+
asyncStorage,
5656
namespace,
5757
sessionContext = true,
5858
foregroundSessionTimeout,
5959
backgroundSessionTimeout,
60-
}: TrackerConfiguration & SessionConfiguration): Promise<SessionPlugin> {
61-
let sessionState = await resumeStoredSession(namespace);
62-
await storeSessionState(namespace, sessionState);
60+
}: TrackerConfiguration & SessionConfiguration & { asyncStorage: AsyncStorage }): Promise<SessionPlugin> {
61+
let sessionState = await resumeStoredSession(namespace, asyncStorage);
62+
await storeSessionState(namespace, sessionState, asyncStorage);
6363

6464
let inBackground = false;
6565
let lastUpdateTs = new Date().getTime();
@@ -84,7 +84,7 @@ export async function newSessionPlugin({
8484
const timeDiff = now.getTime() - lastUpdateTs;
8585
if (timeDiff > getTimeoutMs()) {
8686
startNewSession();
87-
storeSessionState(namespace, sessionState);
87+
storeSessionState(namespace, sessionState, asyncStorage);
8888
}
8989
lastUpdateTs = now.getTime();
9090

Diff for: trackers/react-native-tracker/src/tracker.ts

+52-26
Original file line numberDiff line numberDiff line change
@@ -33,38 +33,64 @@ import { newPlatformContextPlugin } from './plugins/platform_context';
3333
import { newAppLifecyclePlugin } from './plugins/app_lifecycle';
3434
import { newAppInstallPlugin } from './plugins/app_install';
3535
import { newAppContextPlugin } from './plugins/app_context';
36+
import DefaultAsyncStorage from '@react-native-async-storage/async-storage';
3637

3738
const initializedTrackers: Record<string, { tracker: ReactNativeTracker; core: TrackerCore }> = {};
3839

40+
type SetPropertiesAsNonNullable<Obj, Properties extends keyof Obj> = Omit<Obj, Properties> & {
41+
[K in Properties]-?: NonNullable<Obj[K]>;
42+
};
43+
44+
type Configuration = TrackerConfiguration &
45+
EmitterConfiguration &
46+
SessionConfiguration &
47+
SubjectConfiguration &
48+
EventStoreConfiguration &
49+
ScreenTrackingConfiguration &
50+
PlatformContextConfiguration &
51+
DeepLinkConfiguration &
52+
AppLifecycleConfiguration;
53+
54+
type NormalizedConfiguration = SetPropertiesAsNonNullable<
55+
Configuration,
56+
'asyncStorage' | 'devicePlatform' | 'encodeBase64' | 'eventStore' | 'plugins'
57+
>;
58+
59+
const normalizeTrackerConfiguration = async (configuration: Configuration): Promise<NormalizedConfiguration> => {
60+
const eventStore = configuration.eventStore ?? (await newReactNativeEventStore(configuration));
61+
const asyncStorage = configuration.asyncStorage ?? DefaultAsyncStorage;
62+
const plugins = configuration.plugins ?? [];
63+
const devicePlatform = configuration.devicePlatform ?? 'mob';
64+
const encodeBase64 = configuration.encodeBase64 ?? false;
65+
66+
return {
67+
...configuration,
68+
devicePlatform,
69+
encodeBase64,
70+
asyncStorage,
71+
eventStore,
72+
plugins,
73+
};
74+
};
75+
3976
/**
4077
* Creates a new tracker instance with the given configuration
4178
* @param configuration - Configuration for the tracker
4279
* @returns Tracker instance
4380
*/
44-
export async function newTracker(
45-
configuration: TrackerConfiguration &
46-
EmitterConfiguration &
47-
SessionConfiguration &
48-
SubjectConfiguration &
49-
EventStoreConfiguration &
50-
ScreenTrackingConfiguration &
51-
PlatformContextConfiguration &
52-
DeepLinkConfiguration &
53-
AppLifecycleConfiguration
54-
): Promise<ReactNativeTracker> {
55-
const { namespace, appId, encodeBase64 = false } = configuration;
56-
if (configuration.eventStore === undefined) {
57-
configuration.eventStore = await newReactNativeEventStore(configuration);
58-
}
81+
export async function newTracker(configuration: Configuration): Promise<ReactNativeTracker> {
82+
const normalizedConfiguration = await normalizeTrackerConfiguration(configuration);
83+
84+
const { namespace, appId, encodeBase64 } = normalizedConfiguration;
5985

60-
const emitter = newEmitter(configuration);
86+
const emitter = newEmitter(normalizedConfiguration);
6187
const callback = (payload: PayloadBuilder): void => {
6288
emitter.input(payload.build());
6389
};
6490

6591
const core = trackerCore({ base64: encodeBase64, callback });
6692

67-
core.setPlatform(configuration.devicePlatform ?? 'mob');
93+
core.setPlatform(normalizedConfiguration.devicePlatform);
6894
core.setTrackerVersion('rn-' + version);
6995
core.setTrackerNamespace(namespace);
7096
if (appId) {
@@ -73,31 +99,31 @@ export async function newTracker(
7399

74100
const { addPlugin } = newPlugins(namespace, core);
75101

76-
const sessionPlugin = await newSessionPlugin(configuration);
102+
const sessionPlugin = await newSessionPlugin(normalizedConfiguration);
77103
addPlugin(sessionPlugin);
78104

79-
const deepLinksPlugin = await newDeepLinksPlugin(configuration, core);
105+
const deepLinksPlugin = newDeepLinksPlugin(normalizedConfiguration, core);
80106
addPlugin(deepLinksPlugin);
81107

82-
const subject = newSubject(core, configuration);
108+
const subject = newSubject(core, normalizedConfiguration);
83109
addPlugin(subject.subjectPlugin);
84110

85-
const screenPlugin = ScreenTrackingPlugin(configuration);
111+
const screenPlugin = ScreenTrackingPlugin(normalizedConfiguration);
86112
addPlugin({ plugin: screenPlugin });
87113

88-
const platformContextPlugin = await newPlatformContextPlugin(configuration);
114+
const platformContextPlugin = await newPlatformContextPlugin(normalizedConfiguration);
89115
addPlugin(platformContextPlugin);
90116

91-
const lifecyclePlugin = await newAppLifecyclePlugin(configuration, core);
117+
const lifecyclePlugin = await newAppLifecyclePlugin(normalizedConfiguration, core);
92118
addPlugin(lifecyclePlugin);
93119

94-
const installPlugin = newAppInstallPlugin(configuration, core);
120+
const installPlugin = newAppInstallPlugin(normalizedConfiguration, core);
95121
addPlugin(installPlugin);
96122

97-
const appContextPlugin = newAppContextPlugin(configuration);
123+
const appContextPlugin = newAppContextPlugin(normalizedConfiguration);
98124
addPlugin(appContextPlugin);
99125

100-
(configuration.plugins ?? []).forEach((plugin) => addPlugin({ plugin }));
126+
normalizedConfiguration.plugins.forEach((plugin) => addPlugin({ plugin }));
101127

102128
const tracker: ReactNativeTracker = {
103129
...newTrackEventFunctions(core),

Diff for: trackers/react-native-tracker/src/types.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import {
77
StructuredEvent,
88
} from '@snowplow/tracker-core';
99

10+
export interface AsyncStorage {
11+
getItem: (key: string) => Promise<string | null>;
12+
setItem: (key: string, value: string) => Promise<void>;
13+
}
14+
1015
/**
1116
* Configuration for the event store
1217
*/
@@ -24,6 +29,15 @@ export interface EventStoreConfiguration {
2429
* @defaultValue true
2530
*/
2631
useAsyncStorageForEventStore?: boolean;
32+
33+
/**
34+
* The Async storage implementation.
35+
* In environments where AsyncStorage is not available or where another kind of storage is used,
36+
* you can provide a custom implementation.
37+
*
38+
* @defaultValue AsyncStorage from {@link https://react-native-async-storage.github.io/async-storage/ @react-native-async-storage/async-storage}
39+
* */
40+
asyncStorage?: AsyncStorage;
2741
}
2842

2943
/**
@@ -918,7 +932,6 @@ export {
918932
ContextGenerator,
919933
ContextFilter,
920934
EventPayloadAndContext,
921-
EventStore,
922935
EventStoreIterator,
923936
EventStorePayload,
924937
TrackerCore,

Diff for: trackers/react-native-tracker/test/event_store.test.ts

+37
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
1+
import AsyncStorage from '@react-native-async-storage/async-storage';
12
import { newReactNativeEventStore } from '../src/event_store';
23

4+
function createAsyncStorageMock() {
5+
const storageState: Record<string, string> = {};
6+
7+
return {
8+
getItem: (key: string) => Promise.resolve(storageState[key] ?? null),
9+
setItem: (key: string, value: string) => {
10+
storageState[key] = value;
11+
12+
return Promise.resolve();
13+
},
14+
};
15+
}
16+
317
describe('React Native event store', () => {
418
it('keeps track of added events', async () => {
519
const eventStore = await newReactNativeEventStore({
20+
asyncStorage: AsyncStorage,
621
namespace: 'test',
722
});
823

@@ -21,9 +36,11 @@ describe('React Native event store', () => {
2136

2237
it('separates event stores by namespace', async () => {
2338
const eventStore1 = await newReactNativeEventStore({
39+
asyncStorage: AsyncStorage,
2440
namespace: 'test1',
2541
});
2642
const eventStore2 = await newReactNativeEventStore({
43+
asyncStorage: AsyncStorage,
2744
namespace: 'test2',
2845
});
2946

@@ -39,13 +56,33 @@ describe('React Native event store', () => {
3956

4057
it('syncs with AsyncStorage', async () => {
4158
const eventStore1 = await newReactNativeEventStore({
59+
asyncStorage: AsyncStorage,
60+
namespace: 'testA',
61+
});
62+
63+
await eventStore1.add({ payload: { e: 'pv' } });
64+
await eventStore1.add({ payload: { e: 'pp' } });
65+
66+
const eventStore2 = await newReactNativeEventStore({
67+
asyncStorage: AsyncStorage,
68+
namespace: 'testA',
69+
});
70+
71+
expect(await eventStore2.count()).toBe(2);
72+
});
73+
74+
it('syncs with the custom async storage implementation', async () => {
75+
const asyncStorage = createAsyncStorageMock();
76+
const eventStore1 = await newReactNativeEventStore({
77+
asyncStorage,
4278
namespace: 'testA',
4379
});
4480

4581
await eventStore1.add({ payload: { e: 'pv' } });
4682
await eventStore1.add({ payload: { e: 'pp' } });
4783

4884
const eventStore2 = await newReactNativeEventStore({
85+
asyncStorage,
4986
namespace: 'testA',
5087
});
5188

0 commit comments

Comments
 (0)