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

Commit

Permalink
fix: deepmerge device context on update (segmentio#543)
Browse files Browse the repository at this point in the history
  • Loading branch information
oscb authored May 18, 2022
1 parent 3b61568 commit 5878043
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 43 deletions.
8 changes: 4 additions & 4 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -286,12 +286,12 @@ PODS:
- React
- RNGestureHandler (2.3.0):
- React-Core
- segment-analytics-react-native (2.1.12):
- segment-analytics-react-native (2.2.0):
- React-Core
- sovran-react-native
- segment-analytics-react-native-plugin-idfa (0.2.1):
- React-Core
- sovran-react-native (0.2.7):
- sovran-react-native (0.2.8):
- React-Core
- Yoga (1.14.0)

Expand Down Expand Up @@ -459,9 +459,9 @@ SPEC CHECKSUMS:
RNCAsyncStorage: b49b4e38a1548d03b74b30e558a1d18465b94be7
RNCMaskedView: 0e1bc4bfa8365eba5fbbb71e07fbdc0555249489
RNGestureHandler: 77d59828d40838c9fabb76a12d2d0a80c006906f
segment-analytics-react-native: 5287504fa5aa60e64dbb497bee5c7eb6f94e5e49
segment-analytics-react-native: d0b24d7b7e6e6968a3558a2c41f61e94420b6797
segment-analytics-react-native-plugin-idfa: 80e5d610f537156833eabea12a1804523355de95
sovran-react-native: 8d549886ad24ab51f8d471a7db83d1a3ace36358
sovran-react-native: e4064b633fd8232055d003460d5816dff87ba8cc
Yoga: 90dcd029e45d8a7c1ff059e8b3c6612ff409061a

PODFILE CHECKSUM: 0c7eb82d495ca56953c50916b7b49e7512632eb6
Expand Down
2 changes: 1 addition & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"@react-native-community/masked-view": "^0.1.11",
"@react-navigation/native": "^6.0.2",
"@react-navigation/stack": "^6.0.7",
"@segment/sovran-react-native": "^0.2.7",
"@segment/sovran-react-native": "^0.2.8",
"react": "17.0.2",
"react-native": "0.67.3",
"react-native-bootsplash": "^3.2.4",
Expand Down
8 changes: 4 additions & 4 deletions example/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1575,10 +1575,10 @@
color "^3.1.3"
warn-once "^0.1.0"

"@segment/sovran-react-native@^0.2.7":
version "0.2.7"
resolved "https://registry.yarnpkg.com/@segment/sovran-react-native/-/sovran-react-native-0.2.7.tgz#5df47d00a862481ab1f3f07bcc4b8e0a737930ae"
integrity sha512-P4pv3yIbUMv1X54TGioZb+8m4DiDCyKBRuyheqbFEblZeZSckI+WuRp/pooQeyHoGBMQbOY/j0yKksY3Yydkvg==
"@segment/sovran-react-native@^0.2.8":
version "0.2.8"
resolved "https://registry.yarnpkg.com/@segment/sovran-react-native/-/sovran-react-native-0.2.8.tgz#76a3c29011f9726a0fa2ac3942fb1d7715816d7e"
integrity sha512-b3a2vfEj2+jb8w/o+rNrJESWUhHEtrRZgydRNg1PEmMDlLeh42T3mDAap4mtGFICRDHU57w2zPeuw+wfs/sk7g==
dependencies:
"@react-native-async-storage/async-storage" "^1.15.15"
ansi-regex "5.0.1"
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"homepage": "https://github.com/segmentio/analytics-react-native#readme",
"dependencies": {
"@react-native-async-storage/async-storage": "^1.15.17",
"@segment/sovran-react-native": "^0.2.7",
"@segment/sovran-react-native": "^0.2.8",
"deepmerge": "^4.2.2",
"js-base64": "^3.7.2",
"nanoid": "^3.1.25"
Expand Down
34 changes: 6 additions & 28 deletions packages/core/src/__tests__/__helpers__/mockSegmentStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
SegmentEvent,
UserInfoState,
} from '../../types';
import { createCallbackManager } from './utils';

type Data = {
isReady: boolean;
Expand Down Expand Up @@ -51,35 +52,12 @@ export class MockSegmentStore implements Storage {
);
}

// Callbacks
private createCallbackManager = <V, R = void>() => {
type Callback = (value: V) => R;
const callbacks: Callback[] = [];

const deregister = (callback: Callback) => {
callbacks.splice(callbacks.indexOf(callback), 1);
};

const register = (callback: Callback) => {
callbacks.push(callback);
return () => {
deregister(callback);
};
};

const run = (value: V) => {
callbacks.forEach((callback) => callback(value));
};

return { register, deregister, run };
};

private callbacks = {
context: this.createCallbackManager<DeepPartial<Context> | undefined>(),
settings: this.createCallbackManager<SegmentAPIIntegrations>(),
events: this.createCallbackManager<SegmentEvent[]>(),
userInfo: this.createCallbackManager<UserInfoState>(),
deepLinkData: this.createCallbackManager<DeepLinkData>(),
context: createCallbackManager<DeepPartial<Context> | undefined>(),
settings: createCallbackManager<SegmentAPIIntegrations>(),
events: createCallbackManager<SegmentEvent[]>(),
userInfo: createCallbackManager<UserInfoState>(),
deepLinkData: createCallbackManager<DeepLinkData>(),
};

readonly isReady = {
Expand Down
23 changes: 23 additions & 0 deletions packages/core/src/__tests__/__helpers__/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const createCallbackManager = <V, R = void>() => {
type Callback = (value: V) => R;
const callbacks: Callback[] = [];

const deregister = (callback: Callback) => {
callbacks.splice(callbacks.indexOf(callback), 1);
};

const register = (callback: Callback) => {
callbacks.push(callback);
return () => {
deregister(callback);
};
};

const run = (value: V) => {
for (const callback of [...callbacks]) {
callback(value);
}
};

return { register, deregister, run };
};
102 changes: 102 additions & 0 deletions packages/core/src/storage/__tests__/sovranStorage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import deepmerge from 'deepmerge';
import { createCallbackManager } from '../../__tests__/__helpers__/utils';
import { SovranStorage } from '../sovranStorage';

jest.mock('@segment/sovran-react-native', () => ({
registerBridgeStore: jest.fn(),
createStore: <T extends {}>(initialState: T) => {
const callbackManager = createCallbackManager<T>();

let store = {
...initialState,
};

return {
subscribe: jest
.fn()
.mockImplementation((callback: (state: T) => void) => {
callbackManager.register(callback);
return () => callbackManager.deregister(callback);
}),
dispatch: jest
.fn()
.mockImplementation(
async (action: (state: T) => T | Promise<T>): Promise<T> => {
store = await action(store);
callbackManager.run(store);
return store;
}
),
getState: jest.fn().mockImplementation(() => ({ ...store })),
};
},
}));

describe('sovranStorage', () => {
it('works', async () => {
// First test that the constructor works correctly
const sovran = new SovranStorage('test');
expect(sovran.isReady.get()).toBe(false);

// Setup a listener for context changes
const contextListener = jest.fn();
sovran.context.onChange(contextListener);

// A basic test that sets up the context data in the store and checks that the listener is called
const appContext = {
app: {
name: 'test',
namespace: 'com.segment',
version: '1.0.0',
},
device: {
manufacturer: 'Apple',
model: 'iPhone X',
name: 'iPhone',
type: 'mobile',
},
};

const newContext = await sovran.context.set(appContext);
expect(newContext).toEqual(appContext);
expect(sovran.context.get()).toEqual(appContext);
expect(contextListener).toHaveBeenCalledWith(appContext);

// Context should be deeply merged to preserve values set by other plugins
const deviceToken = {
device: {
token: '123',
},
};

const expected = deepmerge(appContext, deviceToken);
const updated = await sovran.context.set(deviceToken);
expect(updated).toEqual(expected);
expect(sovran.context.get()).toEqual(expected);
expect(contextListener).toHaveBeenCalledWith(expected);

// Now lets test the settings, settings are not deeply merged, only merged at the top level
const settings = {
segment: {
apiKey: '123',
},
};

const newSettings = await sovran.settings.set(settings);
expect(newSettings).toEqual(settings);
expect(sovran.settings.get()).toEqual(settings);

const settingsUpdate = {
segment: {
key: '123',
},
braze: {
key: '123',
},
};

const updatedSettings = await sovran.settings.set(settingsUpdate);
expect(updatedSettings).toEqual(settingsUpdate);
expect(sovran.settings.get()).toEqual(settingsUpdate);
});
});
3 changes: 2 additions & 1 deletion packages/core/src/storage/sovranStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
registerBridgeStore,
Store,
} from '@segment/sovran-react-native';
import deepmerge from 'deepmerge';
import type {
SegmentAPIIntegrations,
IntegrationSettings,
Expand Down Expand Up @@ -162,7 +163,7 @@ export class SovranStorage implements Storage {
this.contextStore.subscribe((store) => callback(store.context)),
set: async (value: DeepPartial<Context>) => {
const { context } = await this.contextStore.dispatch((state) => {
return { context: { ...state.context, ...value } };
return { context: deepmerge(state.context, value) };
});
return context;
},
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2141,10 +2141,10 @@
conventional-recommended-bump "^6.1.0"
prepend-file "^2.0.0"

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

0 comments on commit 5878043

Please sign in to comment.