Skip to content

Commit

Permalink
feat: add device info and uuid provider options
Browse files Browse the repository at this point in the history
  • Loading branch information
oscb committed Apr 2, 2024
1 parent 5d2c217 commit 98af055
Show file tree
Hide file tree
Showing 20 changed files with 282 additions and 163 deletions.
107 changes: 88 additions & 19 deletions README.md

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions examples/AnalyticsReactNativeExample/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import {Logger} from './plugins/Logger';
// import { ClevertapPlugin } from '@segment/analytics-react-native-plugin-clevertap';
// import { BrazePlugin } from '@segment/analytics-react-native-plugin-braze';

let startingUUID = 0;

const segmentClient = createClient({
writeKey: '<WRITE_KEY>',
trackAppLifecycleEvents: true,
Expand All @@ -43,6 +45,32 @@ const segmentClient = createClient({
// new TimerFlushPolicy(1000),
// new StartupFlushPolicy(),
],
deviceInfoProvider: async config => {
return {
appName: 'Test Example',
appVersion: '0.1',
buildNumber: '1',
bundleId: 'com.segment.testing.custom.device.info',
locale: 'en_US',
networkType: 'wifi',
osName: 'iOS',
osVersion: '20.0',
screenHeight: 800,
screenWidth: 600,
screenDensity: 2.625,
timezone: 'Europe/London',
manufacturer: 'Apple',
model: 'x86_64',
deviceName: 'iPhone',
deviceId: '123-456-789',
deviceType: 'phone',
};
},
// uuidProvider: () => {
// let next = startingUUID.toString();
// startingUUID++;
// return next;
// },
});

const LoggerPlugin = new Logger();
Expand Down
6 changes: 0 additions & 6 deletions examples/AnalyticsReactNativeExample/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,6 @@ PODS:
- React-jsinspector (0.72.9)
- React-logger (0.72.9):
- glog
- react-native-get-random-values (1.10.0):
- React-Core
- react-native-safe-area-context (4.7.4):
- React-Core
- React-NativeModulesApple (0.72.9):
Expand Down Expand Up @@ -553,7 +551,6 @@ DEPENDENCIES:
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
Expand Down Expand Up @@ -639,8 +636,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/jsinspector"
React-logger:
:path: "../node_modules/react-native/ReactCommon/logger"
react-native-get-random-values:
:path: "../node_modules/react-native-get-random-values"
react-native-safe-area-context:
:path: "../node_modules/react-native-safe-area-context"
React-NativeModulesApple:
Expand Down Expand Up @@ -724,7 +719,6 @@ SPEC CHECKSUMS:
React-jsiexecutor: 5a169b1dd1abad06bed40ab7e1aca883c657d865
React-jsinspector: 54205b269da20c51417e0fc02c4cde9f29a4bf1a
React-logger: f42d2f2bc4cbb5d19d7c0ce84b8741b1e54e88c8
react-native-get-random-values: 384787fd76976f5aec9465aff6fa9e9129af1e74
react-native-safe-area-context: 2cd91d532de12acdb0a9cbc8d43ac72a8e4c897c
React-NativeModulesApple: 9f72feb8a04020b32417f768a7e1e40eec91fef4
React-perflogger: cb433f318c6667060fc1f62e26eb58d6eb30a627
Expand Down
1 change: 0 additions & 1 deletion examples/AnalyticsReactNativeExample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"react": "18.2.0",
"react-native": "0.72.9",
"react-native-gesture-handler": "^2.13.4",
"react-native-get-random-values": "^1.9.0",
"react-native-safe-area-context": "^4.7.4",
"react-native-screens": "^3.27.0"
},
Expand Down
19 changes: 0 additions & 19 deletions examples/AnalyticsReactNativeExample/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2043,7 +2043,6 @@ __metadata:
react: "npm:18.2.0"
react-native: "npm:0.72.9"
react-native-gesture-handler: "npm:^2.13.4"
react-native-get-random-values: "npm:^1.9.0"
react-native-safe-area-context: "npm:^4.7.4"
react-native-screens: "npm:^3.27.0"
react-test-renderer: "npm:18.2.0"
Expand Down Expand Up @@ -3640,13 +3639,6 @@ __metadata:
languageName: node
linkType: hard

"fast-base64-decode@npm:^1.0.0":
version: 1.0.0
resolution: "fast-base64-decode@npm:1.0.0"
checksum: 10c0/6d8feab513222a463d1cb58d24e04d2e04b0791ac6559861f99543daaa590e2636d040d611b40a50799bfb5c5304265d05e3658b5adf6b841a50ef6bf833d821
languageName: node
linkType: hard

"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3":
version: 3.1.3
resolution: "fast-deep-equal@npm:3.1.3"
Expand Down Expand Up @@ -6473,17 +6465,6 @@ __metadata:
languageName: node
linkType: hard

"react-native-get-random-values@npm:^1.9.0":
version: 1.10.0
resolution: "react-native-get-random-values@npm:1.10.0"
dependencies:
fast-base64-decode: "npm:^1.0.0"
peerDependencies:
react-native: ">=0.56"
checksum: 10c0/ec601ffefae3b08314182765168e11d2dae412962a2e3d7b6b7fe5a15ed867ee75eaf68057ddcea1088cbcaa484e22298a7adb51adc6b2facd8744b575007bb5
languageName: node
linkType: hard

"react-native-safe-area-context@npm:^4.7.4":
version: 4.7.4
resolution: "react-native-safe-area-context@npm:4.7.4"
Expand Down
6 changes: 3 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@
"dependencies": {
"@segment/tsub": "^2",
"deepmerge": "^4.3.1",
"js-base64": "^3.7.5",
"uuid": "^9.0.1"
"js-base64": "^3.7.5"
},
"devDependencies": {
"@types/uuid": "^9.0.7",
Expand All @@ -60,7 +59,8 @@
"@react-native-async-storage/async-storage": "1.x",
"react": "*",
"react-native": "*",
"react-native-get-random-values": "1.x"
"react-native-get-random-values": "1.x",
"uuid": "9.x"
},
"peerDependenciesMeta": {
"@react-native-async-storage/async-storage": {
Expand Down
31 changes: 1 addition & 30 deletions packages/core/src/__tests__/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,8 @@
import {
Context,
EventType,
SegmentAPIIntegrations,
TrackEventType,
UserTraits,
} from '../types';
import { uploadEvents } from '../api';
import * as context from '../context';
import { EventType, TrackEventType } from '../types';

describe('#sendEvents', () => {
beforeEach(() => {
jest
.spyOn(context, 'getContext')
.mockImplementationOnce(
async (userTraits?: UserTraits): Promise<Context> => {
return {
traits: userTraits ?? {},
} as Context;
}
);

jest
.spyOn(Date.prototype, 'toISOString')
.mockReturnValue('2001-01-01T00:00:00.000Z');
Expand All @@ -40,20 +23,8 @@ describe('#sendEvents', () => {
messageId: '1d1744bf-5beb-41ac-ad7a-943eac33babc',
};

// Context and Integration exist on SegmentEvents but are transmitted separately to avoid duplication
const additionalEventProperties: {
context: Context;
integrations: SegmentAPIIntegrations;
} = {
context: await context.getContext({ name: 'Hello' }),
integrations: {
Firebase: false,
},
};

const event = {
...serializedEventProperties,
...additionalEventProperties,
};

await uploadEvents({
Expand Down
37 changes: 14 additions & 23 deletions packages/core/src/__tests__/context.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NativeModules } from 'react-native';
import type { Context, NativeContextInfo, UserTraits } from '../types';
import type { Context, NativeContextInfo } from '../types';

import packageJson from '../../package.json';

Expand Down Expand Up @@ -78,34 +78,25 @@ describe('#getContext', () => {
});

it('gets the context', async () => {
const context = await getContext(undefined);
const context = await getContext({
collectDeviceId: false,
uuidProvider: () => UUID,
deviceInfoProvider: AnalyticsReactNativeModule!.getContextInfo,
});

expect(AnalyticsReactNativeModule?.getContextInfo).toHaveBeenCalledTimes(1);
expect(context).toEqual(contextResult);
});

it('gets the context with Traits', async () => {
const userTraits: UserTraits = {
firstName: 'John',
lastName: 'Doe',
};

const context = await getContext(userTraits);

expect(AnalyticsReactNativeModule?.getContextInfo).toHaveBeenCalledTimes(1);
expect(context).toEqual({ ...contextResult, traits: userTraits });
});

it('strip non-required config from native calls', async () => {
await getContext(undefined, {
writeKey: 'notRequiredInNative',
collectDeviceId: true,
flushPolicies: [], // Shouldn't get to native as this is RN specific
it('supports custom implementations', async () => {
const customProvider = jest.fn().mockResolvedValue(mockNativeContext);
const context = await getContext({
collectDeviceId: false,
uuidProvider: () => UUID,
deviceInfoProvider: customProvider,
});

expect(AnalyticsReactNativeModule?.getContextInfo).toHaveBeenCalledTimes(1);
expect(AnalyticsReactNativeModule?.getContextInfo).toHaveBeenCalledWith({
collectDeviceId: true,
});
expect(customProvider).toHaveBeenCalledTimes(1);
expect(context).toEqual(contextResult);
});
});
56 changes: 40 additions & 16 deletions packages/core/src/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,61 +5,68 @@ import {
AppStateStatus,
NativeEventSubscription,
} from 'react-native';
import type {
DeviceInfoProvider,
SegmentAPIConsentSettings,
UUIDProvider,
} from '.';
import {
defaultFlushAt,
defaultFlushInterval,
settingsCDN,
workspaceDestinationFilterKey,
defaultFlushInterval,
defaultFlushAt,
} from './constants';
import { getContext } from './context';
import { defaultContext, getContext } from './context';
import {
ErrorType,
SegmentError,
checkResponseForErrors,
translateHTTPError,
} from './errors';
import {
createAliasEvent,
createGroupEvent,
createIdentifyEvent,
createScreenEvent,
createTrackEvent,
} from './events';
import type { FlushPolicy } from './flushPolicies';
import {
CountFlushPolicy,
Observable,
TimerFlushPolicy,
} from './flushPolicies';
import { FlushPolicyExecuter } from './flushPolicies/flush-policy-executer';
import { AnalyticsReactNativeModule } from './native-module';
import type { DestinationPlugin, PlatformPlugin, Plugin } from './plugin';
import { SegmentDestination } from './plugins/SegmentDestination';
import {
createGetter,
DeepLinkData,
Settable,
Storage,
Watchable,
createGetter,
} from './storage';
import { Timeline } from './timeline';
import { DestinationFilters, EventType, SegmentAPISettings } from './types';
import {
Config,
Context,
DeepPartial,
DestinationFilters,
EventType,
GroupTraits,
IntegrationSettings,
JsonMap,
LoggerType,
PluginType,
SegmentAPIIntegrations,
SegmentAPISettings,
SegmentEvent,
UserInfoState,
UserTraits,
} from './types';
import { allSettled, getPluginsWithFlush, getPluginsWithReset } from './util';
import { getUUID } from './uuid';
import type { FlushPolicy } from './flushPolicies';
import {
checkResponseForErrors,
ErrorType,
SegmentError,
translateHTTPError,
} from './errors';
import type { SegmentAPIConsentSettings } from '.';

type OnPluginAddedCallback = (plugin: Plugin) => void;

Expand Down Expand Up @@ -132,6 +139,8 @@ export class SegmentClient {

readonly deepLinkData: Watchable<DeepLinkData>;

readonly deviceInfoProvider: DeviceInfoProvider;
readonly uuidProvider: UUIDProvider;
// private telemetry?: Telemetry;

/**
Expand Down Expand Up @@ -166,16 +175,26 @@ export class SegmentClient {
config,
logger,
store,
uuidProvider,
}: {
config: Config;
logger: LoggerType;
store: Storage;
uuidProvider?: UUIDProvider;
}) {
this.logger = logger;
this.config = config;
this.store = store;
this.timeline = new Timeline();

this.deviceInfoProvider =
this.config.deviceInfoProvider ??
AnalyticsReactNativeModule?.getContextInfo ??
(() => Promise.resolve({ ...defaultContext }));

// eslint-disable-next-line @typescript-eslint/no-var-requires
this.uuidProvider = uuidProvider ?? require('./uuid').getUUID;

// Initialize the watchables
this.context = {
get: this.store.context.get,
Expand Down Expand Up @@ -599,7 +618,11 @@ export class SegmentClient {
* Application Opened - the previously detected version is same as the current version
*/
private async checkInstalledVersion() {
const context = await getContext(undefined, this.config);
const context = await getContext({
collectDeviceId: this.config?.collectDeviceId ?? false,
deviceInfoProvider: this.deviceInfoProvider,
uuidProvider: this.uuidProvider,
});

const previousContext = this.store.context.get();

Expand Down Expand Up @@ -692,7 +715,8 @@ export class SegmentClient {
async reset(resetAnonymousId = true) {
try {
const { anonymousId: currentId } = await this.store.userInfo.get(true);
const anonymousId = resetAnonymousId === true ? getUUID() : currentId;
const anonymousId =
resetAnonymousId === true ? this.uuidProvider() : currentId;

await this.store.userInfo.set({
anonymousId,
Expand Down Expand Up @@ -807,7 +831,7 @@ export class SegmentClient {
private applyRawEventData = (event: SegmentEvent): SegmentEvent => {
return {
...event,
messageId: getUUID(),
messageId: this.uuidProvider(),
timestamp: new Date().toISOString(),
integrations: event.integrations ?? {},
} as SegmentEvent;
Expand Down
Loading

0 comments on commit 98af055

Please sign in to comment.