Skip to content
This repository was archived by the owner on Jun 27, 2023. It is now read-only.

Commit 5878043

Browse files
authored
fix: deepmerge device context on update (segmentio#543)
1 parent 3b61568 commit 5878043

File tree

9 files changed

+147
-43
lines changed

9 files changed

+147
-43
lines changed

example/ios/Podfile.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -286,12 +286,12 @@ PODS:
286286
- React
287287
- RNGestureHandler (2.3.0):
288288
- React-Core
289-
- segment-analytics-react-native (2.1.12):
289+
- segment-analytics-react-native (2.2.0):
290290
- React-Core
291291
- sovran-react-native
292292
- segment-analytics-react-native-plugin-idfa (0.2.1):
293293
- React-Core
294-
- sovran-react-native (0.2.7):
294+
- sovran-react-native (0.2.8):
295295
- React-Core
296296
- Yoga (1.14.0)
297297

@@ -459,9 +459,9 @@ SPEC CHECKSUMS:
459459
RNCAsyncStorage: b49b4e38a1548d03b74b30e558a1d18465b94be7
460460
RNCMaskedView: 0e1bc4bfa8365eba5fbbb71e07fbdc0555249489
461461
RNGestureHandler: 77d59828d40838c9fabb76a12d2d0a80c006906f
462-
segment-analytics-react-native: 5287504fa5aa60e64dbb497bee5c7eb6f94e5e49
462+
segment-analytics-react-native: d0b24d7b7e6e6968a3558a2c41f61e94420b6797
463463
segment-analytics-react-native-plugin-idfa: 80e5d610f537156833eabea12a1804523355de95
464-
sovran-react-native: 8d549886ad24ab51f8d471a7db83d1a3ace36358
464+
sovran-react-native: e4064b633fd8232055d003460d5816dff87ba8cc
465465
Yoga: 90dcd029e45d8a7c1ff059e8b3c6612ff409061a
466466

467467
PODFILE CHECKSUM: 0c7eb82d495ca56953c50916b7b49e7512632eb6

example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"@react-native-community/masked-view": "^0.1.11",
2525
"@react-navigation/native": "^6.0.2",
2626
"@react-navigation/stack": "^6.0.7",
27-
"@segment/sovran-react-native": "^0.2.7",
27+
"@segment/sovran-react-native": "^0.2.8",
2828
"react": "17.0.2",
2929
"react-native": "0.67.3",
3030
"react-native-bootsplash": "^3.2.4",

example/yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,10 +1575,10 @@
15751575
color "^3.1.3"
15761576
warn-once "^0.1.0"
15771577

1578-
"@segment/sovran-react-native@^0.2.7":
1579-
version "0.2.7"
1580-
resolved "https://registry.yarnpkg.com/@segment/sovran-react-native/-/sovran-react-native-0.2.7.tgz#5df47d00a862481ab1f3f07bcc4b8e0a737930ae"
1581-
integrity sha512-P4pv3yIbUMv1X54TGioZb+8m4DiDCyKBRuyheqbFEblZeZSckI+WuRp/pooQeyHoGBMQbOY/j0yKksY3Yydkvg==
1578+
"@segment/sovran-react-native@^0.2.8":
1579+
version "0.2.8"
1580+
resolved "https://registry.yarnpkg.com/@segment/sovran-react-native/-/sovran-react-native-0.2.8.tgz#76a3c29011f9726a0fa2ac3942fb1d7715816d7e"
1581+
integrity sha512-b3a2vfEj2+jb8w/o+rNrJESWUhHEtrRZgydRNg1PEmMDlLeh42T3mDAap4mtGFICRDHU57w2zPeuw+wfs/sk7g==
15821582
dependencies:
15831583
"@react-native-async-storage/async-storage" "^1.15.15"
15841584
ansi-regex "5.0.1"

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"homepage": "https://github.com/segmentio/analytics-react-native#readme",
4747
"dependencies": {
4848
"@react-native-async-storage/async-storage": "^1.15.17",
49-
"@segment/sovran-react-native": "^0.2.7",
49+
"@segment/sovran-react-native": "^0.2.8",
5050
"deepmerge": "^4.2.2",
5151
"js-base64": "^3.7.2",
5252
"nanoid": "^3.1.25"

packages/core/src/__tests__/__helpers__/mockSegmentStore.ts

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
SegmentEvent,
99
UserInfoState,
1010
} from '../../types';
11+
import { createCallbackManager } from './utils';
1112

1213
type Data = {
1314
isReady: boolean;
@@ -51,35 +52,12 @@ export class MockSegmentStore implements Storage {
5152
);
5253
}
5354

54-
// Callbacks
55-
private createCallbackManager = <V, R = void>() => {
56-
type Callback = (value: V) => R;
57-
const callbacks: Callback[] = [];
58-
59-
const deregister = (callback: Callback) => {
60-
callbacks.splice(callbacks.indexOf(callback), 1);
61-
};
62-
63-
const register = (callback: Callback) => {
64-
callbacks.push(callback);
65-
return () => {
66-
deregister(callback);
67-
};
68-
};
69-
70-
const run = (value: V) => {
71-
callbacks.forEach((callback) => callback(value));
72-
};
73-
74-
return { register, deregister, run };
75-
};
76-
7755
private callbacks = {
78-
context: this.createCallbackManager<DeepPartial<Context> | undefined>(),
79-
settings: this.createCallbackManager<SegmentAPIIntegrations>(),
80-
events: this.createCallbackManager<SegmentEvent[]>(),
81-
userInfo: this.createCallbackManager<UserInfoState>(),
82-
deepLinkData: this.createCallbackManager<DeepLinkData>(),
56+
context: createCallbackManager<DeepPartial<Context> | undefined>(),
57+
settings: createCallbackManager<SegmentAPIIntegrations>(),
58+
events: createCallbackManager<SegmentEvent[]>(),
59+
userInfo: createCallbackManager<UserInfoState>(),
60+
deepLinkData: createCallbackManager<DeepLinkData>(),
8361
};
8462

8563
readonly isReady = {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export const createCallbackManager = <V, R = void>() => {
2+
type Callback = (value: V) => R;
3+
const callbacks: Callback[] = [];
4+
5+
const deregister = (callback: Callback) => {
6+
callbacks.splice(callbacks.indexOf(callback), 1);
7+
};
8+
9+
const register = (callback: Callback) => {
10+
callbacks.push(callback);
11+
return () => {
12+
deregister(callback);
13+
};
14+
};
15+
16+
const run = (value: V) => {
17+
for (const callback of [...callbacks]) {
18+
callback(value);
19+
}
20+
};
21+
22+
return { register, deregister, run };
23+
};
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import deepmerge from 'deepmerge';
2+
import { createCallbackManager } from '../../__tests__/__helpers__/utils';
3+
import { SovranStorage } from '../sovranStorage';
4+
5+
jest.mock('@segment/sovran-react-native', () => ({
6+
registerBridgeStore: jest.fn(),
7+
createStore: <T extends {}>(initialState: T) => {
8+
const callbackManager = createCallbackManager<T>();
9+
10+
let store = {
11+
...initialState,
12+
};
13+
14+
return {
15+
subscribe: jest
16+
.fn()
17+
.mockImplementation((callback: (state: T) => void) => {
18+
callbackManager.register(callback);
19+
return () => callbackManager.deregister(callback);
20+
}),
21+
dispatch: jest
22+
.fn()
23+
.mockImplementation(
24+
async (action: (state: T) => T | Promise<T>): Promise<T> => {
25+
store = await action(store);
26+
callbackManager.run(store);
27+
return store;
28+
}
29+
),
30+
getState: jest.fn().mockImplementation(() => ({ ...store })),
31+
};
32+
},
33+
}));
34+
35+
describe('sovranStorage', () => {
36+
it('works', async () => {
37+
// First test that the constructor works correctly
38+
const sovran = new SovranStorage('test');
39+
expect(sovran.isReady.get()).toBe(false);
40+
41+
// Setup a listener for context changes
42+
const contextListener = jest.fn();
43+
sovran.context.onChange(contextListener);
44+
45+
// A basic test that sets up the context data in the store and checks that the listener is called
46+
const appContext = {
47+
app: {
48+
name: 'test',
49+
namespace: 'com.segment',
50+
version: '1.0.0',
51+
},
52+
device: {
53+
manufacturer: 'Apple',
54+
model: 'iPhone X',
55+
name: 'iPhone',
56+
type: 'mobile',
57+
},
58+
};
59+
60+
const newContext = await sovran.context.set(appContext);
61+
expect(newContext).toEqual(appContext);
62+
expect(sovran.context.get()).toEqual(appContext);
63+
expect(contextListener).toHaveBeenCalledWith(appContext);
64+
65+
// Context should be deeply merged to preserve values set by other plugins
66+
const deviceToken = {
67+
device: {
68+
token: '123',
69+
},
70+
};
71+
72+
const expected = deepmerge(appContext, deviceToken);
73+
const updated = await sovran.context.set(deviceToken);
74+
expect(updated).toEqual(expected);
75+
expect(sovran.context.get()).toEqual(expected);
76+
expect(contextListener).toHaveBeenCalledWith(expected);
77+
78+
// Now lets test the settings, settings are not deeply merged, only merged at the top level
79+
const settings = {
80+
segment: {
81+
apiKey: '123',
82+
},
83+
};
84+
85+
const newSettings = await sovran.settings.set(settings);
86+
expect(newSettings).toEqual(settings);
87+
expect(sovran.settings.get()).toEqual(settings);
88+
89+
const settingsUpdate = {
90+
segment: {
91+
key: '123',
92+
},
93+
braze: {
94+
key: '123',
95+
},
96+
};
97+
98+
const updatedSettings = await sovran.settings.set(settingsUpdate);
99+
expect(updatedSettings).toEqual(settingsUpdate);
100+
expect(sovran.settings.get()).toEqual(settingsUpdate);
101+
});
102+
});

packages/core/src/storage/sovranStorage.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
registerBridgeStore,
44
Store,
55
} from '@segment/sovran-react-native';
6+
import deepmerge from 'deepmerge';
67
import type {
78
SegmentAPIIntegrations,
89
IntegrationSettings,
@@ -162,7 +163,7 @@ export class SovranStorage implements Storage {
162163
this.contextStore.subscribe((store) => callback(store.context)),
163164
set: async (value: DeepPartial<Context>) => {
164165
const { context } = await this.contextStore.dispatch((state) => {
165-
return { context: { ...state.context, ...value } };
166+
return { context: deepmerge(state.context, value) };
166167
});
167168
return context;
168169
},

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2141,10 +2141,10 @@
21412141
conventional-recommended-bump "^6.1.0"
21422142
prepend-file "^2.0.0"
21432143

2144-
"@segment/sovran-react-native@^0.2.7":
2145-
version "0.2.7"
2146-
resolved "https://registry.yarnpkg.com/@segment/sovran-react-native/-/sovran-react-native-0.2.7.tgz#5df47d00a862481ab1f3f07bcc4b8e0a737930ae"
2147-
integrity sha512-P4pv3yIbUMv1X54TGioZb+8m4DiDCyKBRuyheqbFEblZeZSckI+WuRp/pooQeyHoGBMQbOY/j0yKksY3Yydkvg==
2144+
"@segment/sovran-react-native@^0.2.8":
2145+
version "0.2.8"
2146+
resolved "https://registry.yarnpkg.com/@segment/sovran-react-native/-/sovran-react-native-0.2.8.tgz#76a3c29011f9726a0fa2ac3942fb1d7715816d7e"
2147+
integrity sha512-b3a2vfEj2+jb8w/o+rNrJESWUhHEtrRZgydRNg1PEmMDlLeh42T3mDAap4mtGFICRDHU57w2zPeuw+wfs/sk7g==
21482148
dependencies:
21492149
"@react-native-async-storage/async-storage" "^1.15.15"
21502150
ansi-regex "5.0.1"

0 commit comments

Comments
 (0)