Skip to content

Commit 5acdb43

Browse files
adonesky1jiexi
andauthored
fix: use merged integration types in analytics (#223)
* fix: use merged integration types in analytics Replace the singular integration analytics field with a shared integration_types array so mixed SDK integrations are preserved in global analytics context. Normalize empty integrationType values to the direct default to avoid emitting invalid analytics payloads. * lint * noop --------- Co-authored-by: Jiexi Luan <jiexiluan@gmail.com>
1 parent 19c1948 commit 5acdb43

7 files changed

Lines changed: 140 additions & 38 deletions

File tree

packages/analytics/src/analytics.test.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ t.describe('Analytics Integration', () => {
2323
dapp_id: 'aave.com',
2424
anon_id: 'bbbc1727-8b85-433a-a26a-e9df70ddc81c',
2525
platform: 'web-desktop',
26-
integration_type: 'direct',
26+
integration_types: ['direct'],
2727
};
2828

2929
t.afterAll(() => {
@@ -79,8 +79,8 @@ t.describe('Analytics Integration', () => {
7979
analytics.setGlobalProperty('anon_id', eventProperties.anon_id);
8080
analytics.setGlobalProperty('platform', eventProperties.platform);
8181
analytics.setGlobalProperty(
82-
'integration_type',
83-
eventProperties.integration_type,
82+
'integration_types',
83+
eventProperties.integration_types,
8484
);
8585
analytics.track('mmconnect_initialized', {
8686
dapp_id: 'dapp_id',
@@ -102,4 +102,42 @@ t.describe('Analytics Integration', () => {
102102

103103
scope.done();
104104
});
105+
106+
t.it('should merge multiple integration_types global updates', async () => {
107+
let captured: EventV2[] = [];
108+
scope = nock('http://127.0.0.3')
109+
.post('/v2/events', (body) => {
110+
captured = body;
111+
return true;
112+
})
113+
.reply(
114+
200,
115+
{ status: 'success' },
116+
{ 'Content-Type': 'application/json' },
117+
);
118+
119+
analytics = new Analytics('http://127.0.0.3');
120+
analytics.enable();
121+
analytics.setGlobalProperty(
122+
'mmconnect_versions',
123+
eventProperties.mmconnect_versions,
124+
);
125+
analytics.setGlobalProperty('anon_id', eventProperties.anon_id);
126+
analytics.setGlobalProperty('platform', eventProperties.platform);
127+
analytics.setGlobalProperty('dapp_id', eventProperties.dapp_id);
128+
analytics.setGlobalProperty('integration_types', ['wagmi']);
129+
analytics.setGlobalProperty('integration_types', ['direct']);
130+
131+
analytics.track('mmconnect_initialized', {});
132+
133+
await new Promise((resolve) => setTimeout(resolve, 300));
134+
135+
const event = captured[0] as MMConnectPayload | undefined;
136+
t.expect(event?.properties).toMatchObject({
137+
dapp_id: eventProperties.dapp_id,
138+
integration_types: ['wagmi', 'direct'],
139+
});
140+
141+
scope.done();
142+
});
105143
});

packages/analytics/src/analytics.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ class Analytics {
3838
key: K,
3939
value: MMConnectProperties[K],
4040
): void {
41+
if (key === 'integration_types') {
42+
const existing = Array.isArray(this.properties.integration_types)
43+
? this.properties.integration_types
44+
: [];
45+
const incoming = Array.isArray(value) ? value : [];
46+
47+
this.properties.integration_types = [
48+
...new Set([...existing, ...incoming]),
49+
];
50+
return;
51+
}
52+
4153
this.properties[key] = value;
4254
}
4355

packages/analytics/src/schema.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ export type components = {
151151
| 'nodejs'
152152
| 'in-app-browser'
153153
| 'react-native';
154-
/** @description Type of integration used by the SDK. */
155-
integration_type: string;
154+
/** @description Types of integrations used by the SDK. */
155+
integration_types: string[];
156156
};
157157
SdkUsedChainEvent: {
158158
/**
@@ -180,8 +180,8 @@ export type components = {
180180
| 'nodejs'
181181
| 'in-app-browser'
182182
| 'react-native';
183-
/** @description Type of integration used by the SDK. */
184-
integration_type: string;
183+
/** @description Types of integrations used by the SDK. */
184+
integration_types: string[];
185185
};
186186
SdkConnectionInitiatedEvent: {
187187
/**
@@ -211,8 +211,8 @@ export type components = {
211211
| 'nodejs'
212212
| 'in-app-browser'
213213
| 'react-native';
214-
/** @description Type of integration used by the SDK. */
215-
integration_type: string;
214+
/** @description Types of integrations used by the SDK. */
215+
integration_types: string[];
216216
};
217217
SdkConnectionEstablishedEvent: {
218218
/**
@@ -242,8 +242,8 @@ export type components = {
242242
| 'nodejs'
243243
| 'in-app-browser'
244244
| 'react-native';
245-
/** @description Type of integration used by the SDK. */
246-
integration_type: string;
245+
/** @description Types of integrations used by the SDK. */
246+
integration_types: string[];
247247
};
248248
SdkConnectionRejectedEvent: {
249249
/**
@@ -273,8 +273,8 @@ export type components = {
273273
| 'nodejs'
274274
| 'in-app-browser'
275275
| 'react-native';
276-
/** @description Type of integration used by the SDK. */
277-
integration_type: string;
276+
/** @description Types of integrations used by the SDK. */
277+
integration_types: string[];
278278
};
279279
SdkConnectionFailedEvent: {
280280
/**
@@ -304,8 +304,8 @@ export type components = {
304304
| 'nodejs'
305305
| 'in-app-browser'
306306
| 'react-native';
307-
/** @description Type of integration used by the SDK. */
308-
integration_type: string;
307+
/** @description Types of integrations used by the SDK. */
308+
integration_types: string[];
309309
};
310310
WalletConnectionRequestReceivedEvent: {
311311
/**
@@ -381,8 +381,8 @@ export type components = {
381381
| 'nodejs'
382382
| 'in-app-browser'
383383
| 'react-native';
384-
/** @description Type of integration used by the SDK. */
385-
integration_type: string;
384+
/** @description Types of integrations used by the SDK. */
385+
integration_types: string[];
386386
};
387387
SdkActionSucceededEvent: {
388388
/**
@@ -410,8 +410,8 @@ export type components = {
410410
| 'nodejs'
411411
| 'in-app-browser'
412412
| 'react-native';
413-
/** @description Type of integration used by the SDK. */
414-
integration_type: string;
413+
/** @description Types of integrations used by the SDK. */
414+
integration_types: string[];
415415
};
416416
SdkActionFailedEvent: {
417417
/**
@@ -439,8 +439,8 @@ export type components = {
439439
| 'nodejs'
440440
| 'in-app-browser'
441441
| 'react-native';
442-
/** @description Type of integration used by the SDK. */
443-
integration_type: string;
442+
/** @description Types of integrations used by the SDK. */
443+
integration_types: string[];
444444
};
445445
SdkActionRejectedEvent: {
446446
/**
@@ -468,8 +468,8 @@ export type components = {
468468
| 'nodejs'
469469
| 'in-app-browser'
470470
| 'react-native';
471-
/** @description Type of integration used by the SDK. */
472-
integration_type: string;
471+
/** @description Types of integrations used by the SDK. */
472+
integration_types: string[];
473473
};
474474
WalletActionReceivedEvent: {
475475
/**
@@ -566,7 +566,7 @@ export type components = {
566566
| 'nodejs'
567567
| 'in-app-browser'
568568
| 'react-native';
569-
integration_type: string;
569+
integration_types: string[];
570570
transport_type?: 'browser' | 'mwp' | 'unknown';
571571
method?: string;
572572
caip_chain_id?: string;

packages/connect-evm/src/connect.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1038,7 +1038,8 @@ export async function createEVMClient(
10381038
supportedNetworks: supportedNetworksCaipChainId,
10391039
},
10401040
analytics: {
1041-
integrationType: options.analytics?.integrationType ?? 'direct',
1041+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
1042+
integrationType: options.analytics?.integrationType || 'direct',
10421043
},
10431044
versions: {
10441045
// typeof guard needed: Metro (React Native) bundles TS source directly,

packages/connect-multichain/src/init.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,58 @@ function testSuite<T extends MultichainOptions>({
230230
},
231231
);
232232

233+
t.it(
234+
`${platform} should update integration_types analytics global when singleton sees a new integration`,
235+
async () => {
236+
const setGlobalSpy = t.vi.spyOn(analytics, 'setGlobalProperty');
237+
238+
sdk = await createSDK(testOptions);
239+
setGlobalSpy.mockClear();
240+
241+
await createSDK({
242+
...testOptions,
243+
analytics: {
244+
...testOptions.analytics,
245+
integrationType: 'wagmi',
246+
},
247+
} as any);
248+
249+
if (platform === 'web' || platform === 'web-mobile') {
250+
t.expect(setGlobalSpy).toHaveBeenCalledWith('integration_types', [
251+
'wagmi',
252+
]);
253+
}
254+
255+
setGlobalSpy.mockRestore();
256+
},
257+
);
258+
259+
t.it(
260+
`${platform} should normalize empty integrationType to direct for analytics globals`,
261+
async () => {
262+
const setGlobalSpy = t.vi.spyOn(analytics, 'setGlobalProperty');
263+
264+
await createSDK({
265+
...testOptions,
266+
analytics: {
267+
...testOptions.analytics,
268+
integrationType: '',
269+
},
270+
} as any);
271+
272+
if (platform === 'web' || platform === 'web-mobile') {
273+
t.expect(setGlobalSpy).toHaveBeenCalledWith('integration_types', [
274+
'direct',
275+
]);
276+
t.expect(setGlobalSpy).not.toHaveBeenCalledWith('integration_types', [
277+
'',
278+
]);
279+
}
280+
281+
setGlobalSpy.mockRestore();
282+
},
283+
);
284+
233285
t.it(
234286
`${platform} Should gracefully handle init errors by just logging them and return non initialized sdk`,
235287
async () => {

packages/connect-multichain/src/multichain/index.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ export class MetaMaskConnectMultichain extends MultichainCore {
141141

142142
constructor(options: MultichainOptions) {
143143
const withDappMetadata = setupDappMetadata(options);
144-
const integrationType = options.analytics?.integrationType ?? 'direct';
144+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
145+
const integrationType = options.analytics?.integrationType || 'direct';
145146
const allOptions = {
146147
...withDappMetadata,
147148
ui: {
@@ -195,6 +196,11 @@ export class MetaMaskConnectMultichain extends MultichainCore {
195196
'mmconnect_versions',
196197
instance.options.versions ?? {},
197198
);
199+
if (options.analytics?.integrationType) {
200+
analytics.setGlobalProperty('integration_types', [
201+
options.analytics.integrationType,
202+
]);
203+
}
198204
if (options.debug) {
199205
enableDebug('metamask-sdk:*');
200206
}
@@ -250,7 +256,9 @@ export class MetaMaskConnectMultichain extends MultichainCore {
250256
analytics.setGlobalProperty('dapp_id', dappId);
251257
analytics.setGlobalProperty('anon_id', anonId);
252258
analytics.setGlobalProperty('platform', platform);
253-
analytics.setGlobalProperty('integration_type', integrationType);
259+
if (integrationType) {
260+
analytics.setGlobalProperty('integration_types', [integrationType]);
261+
}
254262
analytics.enable();
255263
}
256264

packages/connect-multichain/src/multichain/utils/analytics.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import type {
77
PlatformType,
88
Scope,
99
StoreClient,
10+
TransportType,
1011
} from '../../domain';
11-
import { getPlatformType, TransportType } from '../../domain';
12+
import { getPlatformType } from '../../domain';
1213

1314
/**
1415
* Checks if an error represents a user rejection.
@@ -49,21 +50,16 @@ export async function getBaseAnalyticsProperties(
4950
mmconnect_versions: Record<string, string>;
5051
dapp_id: string;
5152
platform: PlatformType;
52-
integration_type: string;
5353
anon_id: string;
5454
}> {
5555
const dappId = getDappId(options.dapp);
5656
const platform = getPlatformType();
5757
const anonId = await storage.getAnonId();
58-
const integrationType =
59-
(options.analytics as { enabled: true; integrationType: string })
60-
?.integrationType ?? TransportType.UNKNOWN;
6158

6259
return {
6360
mmconnect_versions: options.versions ?? {},
6461
dapp_id: dappId,
6562
platform,
66-
integration_type: integrationType,
6763
anon_id: anonId,
6864
};
6965
}
@@ -86,22 +82,17 @@ export async function getWalletActionAnalyticsProperties(
8682
mmconnect_versions: Record<string, string>;
8783
dapp_id: string;
8884
method: string;
89-
integration_type: string;
9085
caip_chain_id: string;
9186
anon_id: string;
9287
transport_type: TransportType;
9388
}> {
9489
const dappId = getDappId(options.dapp);
9590
const anonId = await storage.getAnonId();
96-
const integrationType =
97-
(options.analytics as { enabled: true; integrationType: string })
98-
?.integrationType ?? 'unknown';
9991

10092
return {
10193
mmconnect_versions: options.versions ?? {},
10294
dapp_id: dappId,
10395
method: invokeOptions.request.method,
104-
integration_type: integrationType,
10596
caip_chain_id: invokeOptions.scope,
10697
anon_id: anonId,
10798
transport_type: transportType,

0 commit comments

Comments
 (0)