Skip to content

Integrate new analytics #1273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"packages/sdk-react-ui",
"packages/sdk-ui",
"packages/sdk-lab",
"packages/sdk-analytics",
"packages/devsocket",
"packages/devreact",
"packages/devexpo",
Expand Down
39 changes: 39 additions & 0 deletions packages/sdk-analytics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# MetaMask SDK Analytics

## Overview

The `@metamask/sdk-analytics` package tracks analytics events for dApps using the MetaMask SDK. It provides a TypeScript-based client for sending events to an analytics API with batching and schema validation.

## Purpose

Enables dApps to:

- Track SDK events (e.g., sdk_initialized, connection_initiated).
- Send events to an analytics API.
- Ensure type safety with OpenAPI schemas.

## Features

- Event Tracking: Supports events like sdk_initialized, sdk_used_chain, and wallet actions.
- Batching: Events are batched for efficient network usage.
- Error Handling: Uses exponential backoff for failed requests.
- Type Safety: Leverages TypeScript and OpenAPI schemas.

## Usage

Import the global client, enable it, set global props and track events.

```typescript
import { analytics } from '@metamask/sdk-analytics';

analytics.enable();

analytics.setGlobalProperty('sdk_version', '1.0.0');
analytics.setGlobalProperty('platform', 'web-desktop');

analytics.track('sdk_initialized', {
dapp_id: 'example.com',
anon_id: 'bbbc1727-8b85-433a-a26a-e9df70ddc81c',
integration_type: 'direct',
});
```
28 changes: 28 additions & 0 deletions packages/sdk-analytics/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const tseslint = require('@typescript-eslint/eslint-plugin');
const tsParser = require('@typescript-eslint/parser');

module.exports = [
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: 2021,
sourceType: 'module',
},
},
plugins: {
'@typescript-eslint': tseslint,
},
rules: {
...tseslint.configs['recommended'].rules,
'indent': ['error', 2],
'linebreak-style': ['error', 'unix'],
'quotes': ['error', 'single'],
'semi': ['error', 'always'],
},
},
{
ignores: ['src/schema.ts', 'dist/**', 'node_modules/**'],
},
];
27 changes: 27 additions & 0 deletions packages/sdk-analytics/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@metamask/sdk-analytics",
"version": "0.0.1",
"description": "Analytics package for MetaMask SDK",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc --build",
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix",
"test": "vitest",
"test:ci": "vitest --run"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.31.0",
"@typescript-eslint/parser": "^8.31.0",
"eslint": "^9.25.1",
"nock": "^14.0.4",
"typescript": "^5.8.3",
"vitest": "^3.1.2"
},
"private": true,
"license": "MIT",
"dependencies": {
"openapi-fetch": "^0.13.5"
}
}
70 changes: 70 additions & 0 deletions packages/sdk-analytics/src/analytics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as t from 'vitest';
import Analytics from './analytics';
import nock from 'nock';
import * as schema from './schema';

t.describe('Analytics Integration', () => {
let analytics: Analytics;
let scope: nock.Scope;

const event: schema.components['schemas']['SdkInitializedEvent'] = {
name: 'sdk_initialized',
sdk_version: '1.0.0',
dapp_id: 'aave.com',
anon_id: 'bbbc1727-8b85-433a-a26a-e9df70ddc81c',
platform: 'web-desktop',
integration_type: 'direct',
};

t.afterAll(() => {
nock.cleanAll();
});

t.it('should do nothing when disabled', async () => {
let captured: Event[] = [];
scope = nock('http://127.0.0.1')
.post('/v1/events', (body) => {
captured = body; // Capture the request body directly
return true; // Accept any body to proceed with the intercept
})
.optionally()
.reply(200, { status: 'success' }, { 'Content-Type': 'application/json' });

analytics = new Analytics('http://127.0.0.1');
analytics.track(event.name, { ...event });

// Wait for the Sender to flush the event (baseIntervalMs = 200ms + buffer)
await new Promise((resolve) => setTimeout(resolve, 300));

// Verify the captured payload
t.expect(captured).toEqual([]);

scope.done();
});

t.it('should track an event when enabled', async () => {
let captured: Event[] = [];
scope = nock('http://127.0.0.2')
.post('/v1/events', (body) => {
captured = body; // Capture the request body directly
return true; // Accept any body to proceed with the intercept
})
.reply(200, { status: 'success' }, { 'Content-Type': 'application/json' });

analytics = new Analytics('http://127.0.0.2');
analytics.enable();
analytics.setGlobalProperty('sdk_version', event.sdk_version);
analytics.setGlobalProperty('anon_id', event.anon_id);
analytics.setGlobalProperty('platform', event.platform);
analytics.setGlobalProperty('integration_type', event.integration_type);
analytics.track(event.name, { dapp_id: 'some-non-global-property' });

// Wait for the Sender to flush the event (baseIntervalMs = 200ms + buffer)
await new Promise((resolve) => setTimeout(resolve, 300));

// Verify the captured payload
t.expect(captured).toEqual([{ ...event, dapp_id: 'some-non-global-property' }]);

scope.done();
});
});
48 changes: 48 additions & 0 deletions packages/sdk-analytics/src/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import createClient from 'openapi-fetch';
import * as schema from './schema';
import Sender from './sender';

type Event = schema.components['schemas']['Event'];

class Analytics {
private enabled: boolean = false;
private sender: Sender<Event>;
private properties: Record<string, string> = {};

constructor(baseUrl: string) {
const client = createClient<schema.paths>({ baseUrl });

const sendFn = async (batch: Event[]) => {
const res = await client.POST('/v1/events', { body: batch });
if (res.response.status !== 200) {
throw new Error(res.error);
}
};

this.sender = new Sender({ batchSize: 100, baseIntervalMs: 200, sendFn });
}

public enable() {
if (this.enabled) return;
this.enabled = true;
this.sender.start();
}

public setGlobalProperty(key: string, value: string) {
this.properties[key] = value;
}

public track<T extends Event>(name: T['name'], properties: Partial<T>) {
if (!this.enabled) return;

const event: Event = {
name: name,
...this.properties,
...properties,
} as T;

this.sender.enqueue(event);
}
}

export default Analytics;
7 changes: 7 additions & 0 deletions packages/sdk-analytics/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Analytics from './analytics';

const client = new Analytics('https://mm-sdk-analytics.api.cx.metamask.io/');

export const analytics = client; // FIXME: use default export

export default client;
Loading
Loading