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

Commit

Permalink
feat: new onContextLoad events and awaitable context updates (segment…
Browse files Browse the repository at this point in the history
…io#532)

- awaitable dispatches to store set
- adds onContextLoaded callback for plugins that require device context being loaded during configuration. (Requested in segmentio#524)
- upgraded sovran to v0.2.7 (Adds awaitable dispatch)
- fixes segmentio#527 where Application Installed/Opened events might had not contained up to date context data as the store set could not be awaited before firing the events.
- chore: reconfigure commitlint
- chore: upgrade husky to v8
  • Loading branch information
oscb authored May 10, 2022
1 parent aea3a76 commit 4508ad4
Show file tree
Hide file tree
Showing 14 changed files with 379 additions and 232 deletions.
4 changes: 4 additions & 0 deletions .husky/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

yarn commitlint --edit $1
4 changes: 4 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

yarn lint && yarn typescript && yarn test
13 changes: 13 additions & 0 deletions commitlint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'header-max-length': [2, 'always', 80],
// Disable all the body and footer max length rules since CommitLint cannot handle multiline text in body and footer making these rules too much of a nuisance
'body-max-length': [0, 'always'],
'body-max-line-length': [0, 'always'],
'footer-max-length': [0, 'always'],
'footer-max-line-length': [0, 'always'],
// Also disable this rule since it will always complain due to the multiline incompatibility
'footer-leading-blank': [0, 'always'],
},
};
6 changes: 3 additions & 3 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ PODS:
- sovran-react-native
- segment-analytics-react-native-plugin-idfa (0.2.1):
- React-Core
- sovran-react-native (0.2.6):
- sovran-react-native (0.2.7):
- React-Core
- Yoga (1.14.0)

Expand Down Expand Up @@ -461,9 +461,9 @@ SPEC CHECKSUMS:
RNGestureHandler: 77d59828d40838c9fabb76a12d2d0a80c006906f
segment-analytics-react-native: 5287504fa5aa60e64dbb497bee5c7eb6f94e5e49
segment-analytics-react-native-plugin-idfa: 80e5d610f537156833eabea12a1804523355de95
sovran-react-native: ef02f663b489ac5e63ea7b80cd8426bf82992263
sovran-react-native: 8d549886ad24ab51f8d471a7db83d1a3ace36358
Yoga: 90dcd029e45d8a7c1ff059e8b3c6612ff409061a

PODFILE CHECKSUM: 0c7eb82d495ca56953c50916b7b49e7512632eb6

COCOAPODS: 1.11.2
COCOAPODS: 1.11.3
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.6",
"@segment/sovran-react-native": "^0.2.7",
"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.6":
version "0.2.6"
resolved "https://registry.yarnpkg.com/@segment/sovran-react-native/-/sovran-react-native-0.2.6.tgz#895ef37b71c299f56c89515cf8d200f13fce2251"
integrity sha512-SxqKvMvgu9PZo0jSkZ0yys08H9qJbI1uGbaeBvJFZfs92TIvQmcW0PYQZsspqt85RPzrL4J9KDio6xoLXmHurw==
"@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==
dependencies:
"@react-native-async-storage/async-storage" "^1.15.15"
ansi-regex "5.0.1"
Expand Down
19 changes: 4 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"packages/plugins/*"
],
"scripts": {
"bootstrap": "yarn install && yarn example install && yarn example pods",
"bootstrap": "yarn install && yarn example install && yarn example pods && husky install",
"core": "yarn workspace @segment/analytics-react-native",
"example": "yarn --cwd example",
"build": "yarn workspaces run build",
Expand All @@ -20,27 +20,16 @@
"<rootDir>/packages/*"
]
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
"pre-commit": "yarn lint && yarn typescript && yarn test"
}
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"devDependencies": {
"@changesets/cli": "^2.16.0",
"@commitlint/config-conventional": "^11.0.0",
"@commitlint/config-conventional": "^16.2.4",
"@react-native-community/eslint-config": "^2.0.0",
"@release-it/conventional-changelog": "^2.0.0",
"commitlint": "^11.0.0",
"commitlint": "^16.2.4",
"eslint": "^7.2.0",
"eslint-config-prettier": "^7.0.0",
"eslint-plugin-prettier": "^3.1.3",
"husky": "^4.2.5",
"husky": "^8.0.0",
"jest": "^27.3.1",
"prettier": "^2.3.2",
"release-it": "14.12.4",
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.6",
"@segment/sovran-react-native": "^0.2.7",
"deepmerge": "^4.2.2",
"js-base64": "^3.7.2",
"nanoid": "^3.1.25"
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/__tests__/__helpers__/mockSegmentStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export class MockSegmentStore implements Storage {
set: (value: DeepPartial<Context>) => {
this.data.context = { ...value };
this.callbacks.context.run(value);
return this.data.context;
},
};

Expand All @@ -110,6 +111,7 @@ export class MockSegmentStore implements Storage {
set: (value: SegmentAPIIntegrations) => {
this.data.settings = value;
this.callbacks.settings.run(value);
return this.data.settings;
},
add: (key: string, value: IntegrationSettings) => {
this.data.settings[key] = value;
Expand Down Expand Up @@ -143,6 +145,7 @@ export class MockSegmentStore implements Storage {
set: (value: UserInfoState) => {
this.data.userInfo = value;
this.callbacks.userInfo.run(value);
return this.data.userInfo;
},
};

Expand Down
23 changes: 23 additions & 0 deletions packages/core/src/__tests__/internal/checkInstalledVersion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,27 @@ describe('internal #checkInstalledVersion', () => {
deepmerge(newContext, injectedContextByPlugins)
);
});

it('executes callback when context is updated in store', async () => {
client = new SegmentClient(clientArgs);
const callback = jest.fn().mockImplementation(() => {
expect(store.context.get()).toEqual(currentContext);
});
client.onContextLoaded(callback);
jest.spyOn(context, 'getContext').mockResolvedValueOnce(currentContext);
await client.init();
expect(callback).toHaveBeenCalled();
});

it('executes callback immediatley if registered after context was already loaded', async () => {
client = new SegmentClient(clientArgs);
jest.spyOn(context, 'getContext').mockResolvedValueOnce(currentContext);
await client.init();
// Register callback after context is loaded
const callback = jest.fn().mockImplementation(() => {
expect(store.context.get()).toEqual(currentContext);
});
client.onContextLoaded(callback);
expect(callback).toHaveBeenCalled();
});
});
30 changes: 29 additions & 1 deletion packages/core/src/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@ import {
SegmentAPIIntegrations,
SegmentAPISettings,
SegmentEvent,
UpdateType,
UserInfoState,
UserTraits,
} from './types';
import { getPluginsWithFlush, getPluginsWithReset } from './util';
import { getUUID } from './uuid';

type OnContextLoadCallback = (type: UpdateType) => void | Promise<void>;

export class SegmentClient {
// the config parameters for the client - a merge of user provided and default options
private config: Config;
Expand Down Expand Up @@ -71,6 +74,10 @@ export class SegmentClient {

private isInitialized = false;

private isContextLoaded = false;

private onContextLoadedCallback: OnContextLoadCallback | undefined;

get platformPlugins() {
const plugins: PlatformPlugin[] = [];

Expand Down Expand Up @@ -568,8 +575,14 @@ export class SegmentClient {
const previousContext = this.store.context.get();

// Only overwrite the previous context values to preserve any values that are added by enrichment plugins like IDFA
this.store.context.set(deepmerge(previousContext ?? {}, context));
await this.store.context.set(deepmerge(previousContext ?? {}, context));

// Only callback during the intial context load
if (this.onContextLoadedCallback !== undefined && !this.isContextLoaded) {
this.onContextLoadedCallback(UpdateType.initial);
}

this.isContextLoaded = true;
if (!this.config.trackAppLifecycleEvents) {
return;
}
Expand Down Expand Up @@ -669,4 +682,19 @@ export class SegmentClient {

this.logger.info('Client has been reset');
}

/**
* Registers a callback for when the client has loaded the device context. This happens at the startup of the app, but
* it is handy for plugins that require context data during configure as it guarantees the context data is available.
*
* If the context is already loaded it will call the callback immediately.
*
* @param callback Function to call when context is ready.
*/
onContextLoaded(callback: OnContextLoadCallback) {
this.onContextLoadedCallback = callback;
if (this.isContextLoaded) {
this.onContextLoadedCallback(UpdateType.initial);
}
}
}
15 changes: 9 additions & 6 deletions packages/core/src/storage/sovranStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,21 +160,23 @@ export class SovranStorage implements Storage {
get: () => this.contextStore.getState().context,
onChange: (callback: (value?: DeepPartial<Context>) => void) =>
this.contextStore.subscribe((store) => callback(store.context)),
set: (value: DeepPartial<Context>) => {
this.contextStore.dispatch((state) => {
set: async (value: DeepPartial<Context>) => {
const { context } = await this.contextStore.dispatch((state) => {
return { context: { ...state.context, ...value } };
});
return context;
},
};
readonly settings = {
get: () => this.settingsStore.getState().settings,
onChange: (
callback: (value?: SegmentAPIIntegrations | undefined) => void
) => this.settingsStore.subscribe((store) => callback(store.settings)),
set: (value: SegmentAPIIntegrations) => {
this.settingsStore.dispatch((state) => {
set: async (value: SegmentAPIIntegrations) => {
const { settings } = await this.settingsStore.dispatch((state) => {
return { settings: { ...state.settings, ...value } };
});
return settings;
},
add: (key: string, value: IntegrationSettings) => {
this.settingsStore.dispatch((state) => ({
Expand Down Expand Up @@ -209,10 +211,11 @@ export class SovranStorage implements Storage {
get: () => this.userInfoStore.getState().userInfo,
onChange: (callback: (value: UserInfoState) => void) =>
this.userInfoStore.subscribe((store) => callback(store.userInfo)),
set: (value: UserInfoState) => {
this.userInfoStore.dispatch((state) => ({
set: async (value: UserInfoState) => {
const { userInfo } = await this.userInfoStore.dispatch((state) => ({
userInfo: { ...state.userInfo, ...value },
}));
return userInfo;
},
};

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/storage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface Watchable<T> {
* Implements a value that can be set
*/
export interface Settable<T> {
set: (value: T) => void;
set: (value: T) => T | Promise<T>;
}

/**
Expand Down
Loading

0 comments on commit 4508ad4

Please sign in to comment.