Skip to content

Commit 6956699

Browse files
committed
feat: ensure platform defaults correctly set on mobile
1 parent 495dda3 commit 6956699

File tree

9 files changed

+100
-30
lines changed

9 files changed

+100
-30
lines changed

apps/mobile/.env.development

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export EXPO_PUBLIC_NODE_ENV="development"
1+
EXPO_PUBLIC_NODE_ENV="development"

apps/mobile/src/store/settings/settings.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export function useSettings() {
114114
void analytics?.track('user_setting_updated', {
115115
analytics: pref,
116116
});
117-
void analytics?.identify({
117+
void analytics?.identify(undefined, {
118118
analytics_preference: pref,
119119
});
120120
},
@@ -129,7 +129,7 @@ export function useSettings() {
129129
void analytics?.track('user_setting_updated', {
130130
email_address: address,
131131
});
132-
void analytics?.identify({
132+
void analytics?.identify(undefined, {
133133
has_email_address: !!address,
134134
});
135135
},
@@ -184,7 +184,7 @@ export function useSettings() {
184184
const network =
185185
networkPreference.chain.bitcoin.bitcoinNetwork === 'mainnet' ? 'testnet' : 'mainnet';
186186
dispatch(userChangedNetworkPreference(network));
187-
void analytics?.identify({
187+
void analytics?.identify(undefined, {
188188
active_network: network,
189189
});
190190
void analytics?.track('user_setting_updated', {
@@ -197,7 +197,7 @@ export function useSettings() {
197197
void analytics?.track('user_setting_updated', {
198198
theme,
199199
});
200-
void analytics?.identify({
200+
void analytics?.identify(undefined, {
201201
active_theme: theme,
202202
});
203203
},
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {
2+
EventType,
3+
PlatformPlugin,
4+
PluginType,
5+
SegmentClient,
6+
SegmentEvent,
7+
} from '@segment/analytics-react-native';
8+
9+
/**
10+
* This plugin is used to ensure that any default event tracks, eg app lifecycle events,
11+
* have the platform property set to 'mobile'.
12+
*
13+
* Docs on these are sparse but this source code is a good reference:
14+
* https://github.com/segmentio/analytics-react-native/tree/master/packages/plugins
15+
*
16+
*/
17+
export class AppLifecycleEventPlugin extends PlatformPlugin {
18+
type = PluginType.before;
19+
20+
configure(analytics: SegmentClient) {
21+
this.analytics = analytics;
22+
}
23+
// Ensures all events have the platform property set to 'mobile', especially default Segment controlled events.'
24+
execute(event: SegmentEvent): SegmentEvent {
25+
if (event.type === EventType.TrackEvent || event.type === EventType.ScreenEvent) {
26+
return { ...event, properties: { ...event.properties, platform: 'mobile' } };
27+
}
28+
return event;
29+
}
30+
}

apps/mobile/src/utils/analytics.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { store } from '@/store';
22
import { selectAnalyticsPreference } from '@/store/settings/settings.read';
3+
import { AppLifecycleEventPlugin } from '@/utils/analytics-plugins';
34
import { SegmentClient, createClient } from '@segment/analytics-react-native';
45

56
import { configureAnalyticsClient } from '@leather.io/analytics';
@@ -10,6 +11,8 @@ const segmentClient = createClient({
1011
debug: false,
1112
});
1213

14+
segmentClient.add({ plugin: new AppLifecycleEventPlugin() });
15+
1316
const leatherAnalyticsClient = configureAnalyticsClient<SegmentClient>({
1417
client: segmentClient,
1518
defaultProperties: {

packages/analytics/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
"build:watch": "tsup --watch --onSuccess 'tsup --dts-only'",
1717
"format": "prettier . --write \"src/**/*.ts\" --ignore-path ../../.prettierignore",
1818
"format:check": "prettier . --check \"src/**/*.ts\" --ignore-path ../../.prettierignore",
19+
"test": "vitest run",
20+
"test:watch": "vitest watch",
1921
"typecheck": "tsc --noEmit"
2022
},
2123
"exports": {

packages/analytics/src/client.spec.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,27 @@
22
import { AnalyticsClient } from './client';
33

44
export const mockExternalAnalyticsClient = {
5-
track: vi.fn().mockResolvedValue(undefined),
6-
screen: vi.fn().mockResolvedValue(undefined),
7-
group: vi.fn().mockResolvedValue(undefined),
8-
identify: vi.fn().mockResolvedValue(undefined),
9-
page: vi.fn().mockResolvedValue(undefined),
10-
register: vi.fn().mockResolvedValue(undefined),
11-
deregister: vi.fn().mockResolvedValue(undefined),
5+
track: vi.fn(),
6+
screen: vi.fn(),
7+
group: vi.fn(),
8+
identify: vi.fn(),
9+
page: vi.fn(),
10+
register: vi.fn(),
11+
deregister: vi.fn(),
1212
};
1313

1414
describe('AnalyticsClient', () => {
15+
afterEach(() => {
16+
vi.clearAllMocks();
17+
});
1518
it('should be able to track all events with default properties', async () => {
16-
const analytics = AnalyticsClient(mockExternalAnalyticsClient, {
19+
const analytics = AnalyticsClient({
20+
client: mockExternalAnalyticsClient,
1721
defaultProperties: { platform: 'web' },
1822
});
1923

2024
await analytics.track('background_analytics_schema_fail');
21-
await analytics.client.screen('/home/screen', undefined);
25+
await analytics.screen('/home/screen');
2226

2327
expect(mockExternalAnalyticsClient.track).toHaveBeenCalledWith(
2428
'background_analytics_schema_fail',
@@ -33,12 +37,13 @@ describe('AnalyticsClient', () => {
3337
});
3438

3539
it('should be able to track group and identify with default traits', async () => {
36-
const analytics = AnalyticsClient(mockExternalAnalyticsClient, {
40+
const analytics = AnalyticsClient({
41+
client: mockExternalAnalyticsClient,
3742
defaultTraits: { user: 'test' },
3843
});
3944

40-
await analytics.client.identify('1df3_34j3');
41-
await analytics.client.group('1df3_34j3');
45+
await analytics.identify('1df3_34j3');
46+
await analytics.group('1df3_34j3');
4247

4348
expect(mockExternalAnalyticsClient.identify).toHaveBeenCalledWith('1df3_34j3', {
4449
user: 'test',
@@ -50,7 +55,8 @@ describe('AnalyticsClient', () => {
5055
});
5156

5257
it('should enforce snake case for untyped track', async () => {
53-
const client = AnalyticsClient(mockExternalAnalyticsClient, {
58+
const client = AnalyticsClient({
59+
client: mockExternalAnalyticsClient,
5460
defaultProperties: { platform: 'web' },
5561
});
5662

packages/analytics/src/client.ts

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,55 @@
11
import { AnalyticsClientConfig, AnalyticsClientInterface, Events, JsonMap } from './types';
22

33
export function AnalyticsClient<T extends AnalyticsClientInterface>(
4-
analyticsClient: T,
5-
options: Pick<AnalyticsClientConfig<T>, 'defaultProperties' | 'defaultTraits'>
4+
config: AnalyticsClientConfig<T>
65
) {
6+
const { client: analyticsClient, defaultProperties = {}, defaultTraits = {} } = config;
7+
78
return {
89
async track<K extends keyof Events>(
910
event: K,
1011
...properties: undefined extends Events[K] ? [] : [param: Events[K]]
1112
) {
12-
return analyticsClient.track(event, { ...properties, ...options.defaultProperties });
13+
return analyticsClient.track(event, { ...properties, ...defaultProperties });
1314
},
14-
async untypedTrack(event: string, properties?: JsonMap) {
15+
16+
async untypedTrack(event: string, properties?: JsonMap | Record<string, unknown>) {
1517
if (event.match(/^[a-zA-Z0-9\s][a-zA-Z0-9\s]*$/)) {
1618
throw new Error('Event must be snake_case');
1719
}
18-
return analyticsClient.track(event as any, { ...properties, ...options.defaultProperties });
20+
return analyticsClient.track(event as any, {
21+
...properties,
22+
...defaultProperties,
23+
});
24+
},
25+
26+
async screen(name: string, properties?: JsonMap | Record<string, unknown>) {
27+
return analyticsClient.screen(name, { ...properties, ...defaultProperties });
28+
},
29+
30+
group(groupId: string, traits?: JsonMap | Record<string, unknown>) {
31+
return analyticsClient.group(groupId, { ...traits, ...defaultTraits });
32+
},
33+
34+
async identify(userId?: string, traits?: JsonMap | Record<string, unknown>) {
35+
return await analyticsClient.identify(userId, {
36+
...traits,
37+
...defaultTraits,
38+
});
39+
},
40+
41+
page(category?: string, name?: string, properties?: JsonMap | Record<string, unknown>) {
42+
if (typeof analyticsClient.page === 'function') {
43+
return analyticsClient.page(category, name, {
44+
...properties,
45+
...defaultProperties,
46+
});
47+
}
48+
return Promise.resolve();
49+
},
50+
51+
get client() {
52+
return analyticsClient;
1953
},
20-
screen: analyticsClient.screen.bind(analyticsClient),
21-
group: analyticsClient.group.bind(analyticsClient),
22-
identify: analyticsClient.identify.bind(analyticsClient),
23-
page: analyticsClient.page ? analyticsClient.page.bind(analyticsClient) : Promise.resolve(),
24-
client: analyticsClient,
2554
};
2655
}

packages/analytics/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ export function configureAnalyticsClient<T extends AnalyticsClientInterface>({
1212
defaultProperties: DefaultProperties;
1313
defaultTraits?: JsonMap;
1414
}) {
15-
return AnalyticsClient<T>(client, { defaultProperties, defaultTraits });
15+
return AnalyticsClient<T>({ client, defaultProperties, defaultTraits });
1616
}

packages/analytics/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export interface AnalyticsClientInterface {
1010
track: (event: string, ...args: any[]) => Promise<any>;
1111
group: (groupId: string, traits?: any, ...args: any[]) => Promise<any>;
1212
identify: (...args: any[]) => Promise<any>;
13-
page?: (name: string, ...args: any[]) => Promise<any>;
13+
page?: (category?: string, name?: string, ...args: any[]) => Promise<any>;
1414
}
1515

1616
export interface AnalyticsClientConfig<T extends AnalyticsClientInterface> {

0 commit comments

Comments
 (0)