Skip to content

Commit

Permalink
refactor: add new analytics package, leather-io/issues#345
Browse files Browse the repository at this point in the history
  • Loading branch information
camerow committed Sep 24, 2024
1 parent 936fa26 commit 737d3fb
Show file tree
Hide file tree
Showing 20 changed files with 2,281 additions and 607 deletions.
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodejs 20.12.2
12 changes: 6 additions & 6 deletions docs/extension-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

The extension will heavily rely on monorepo packages. Soon, we will need to apply changes to these packages as part of the work on the extension repository, thus necessitating a way to test changes made to the monorepo within the extension.

### Hard Linking
## Hard Linking

#### Initialization
### Initialization

One method to achieve this is through hard linking. Here is a step-by-step guide to help you set it up correctly:

1. Clone the repositories:

```
$ git clone https://github.com/leather-io/mono
$ git clone https://github.com/leather-io/extension
```git
git clone https://github.com/leather-io/mono
git clone https://github.com/leather-io/extension
```

For simplicity, do this in the same folder so that `mono` and `extension` are sibling directories.
Expand Down Expand Up @@ -57,7 +57,7 @@ That's it; the setup should now be fully functional.

Now, when we update a package in the `mono` repo, it doesn't automatically move the changes to the `extension`'s `node_modules`. To do this, run the following command in the extension each time you update a package in the monorepo:

```
```sh
pnpm i
```

Expand Down
1 change: 1 addition & 0 deletions packages/analytics/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SEGMENT_API_KEY='<your-segment-api-key>'
11 changes: 11 additions & 0 deletions packages/analytics/.eslintrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
extends:
- '@leather.io/eslint-config'
- universe/native
parserOptions:
project: ./tsconfig.json
ignorePatterns:
- .eslintrc.js
- '*.js'
rules:
import/order: 0
no-void: 0
18 changes: 18 additions & 0 deletions packages/analytics/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# build
.tsup
# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files
.env*.local

# typescript
*.tsbuildinfo
dist-web/
dist-native/
55 changes: 55 additions & 0 deletions packages/analytics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# @leather/analytics

This package provides a client for sending analytics events to various analytics services. Currently, it supports Segment but could be extended to support other services in the future.

## Installation

```bash
pnpm install @leather/analytics
```

## Usage

Before making any analytics calls, you must configure the analytics client with your write key.

```ts
import { configureAnalyticsClient } from '@leather/analytics';

configureAnalyticsClient({
writeKey: 'YOUR_WRITE_KEY',
defaultProperties: {
platform: 'web',
},
});
```

Now you can make analytics calls.

```ts
import { analyticsClient } from '@leather/analytics';

analyticsClient.track('My Event', {
property: 'value',
});
```

You can also inject your own analytics client if you are using a custom analytics service or want to use a stubbed client for testing.

````ts
import { configureAnalyticsClient } from '@leather/analytics';

configureAnalyticsClient({
client: myAnalyticsClient,
});

## Development

```bash
pnpm build
````
or
```bash
pnpm build:watch
```
52 changes: 52 additions & 0 deletions packages/analytics/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "@leather.io/analytics",
"author": "leather.io",
"description": "Analytics package for Leather using Segment",
"version": "0.0.1",
"license": "MIT",
"type": "module",
"scripts": {
"build": "pnpm build:native && pnpm build:web",
"build:native": "tsup --config tsup.config.native.ts",
"build:watch": "concurrently \"pnpm build:native --watch\" \"pnpm build:web --watch\" ",
"build:web": "tsup --config tsup.config.web.ts",
"format": "prettier . --write \"src/**/*.ts\" --ignore-path ../../.prettierignore",
"format:check": "prettier . --check \"src/**/*.ts\" --ignore-path ../../.prettierignore",
"lint": "eslint . --ignore-path ../../.eslintignore",
"lint:fix": "eslint . --fix --ignore-path ../../.eslintignore",
"typecheck": "tsc --noEmit --project ./tsconfig.json"
},
"files": [
"dist-web",
"dist-native"
],
"exports": {
".": {
"import": "./dist-web/src/index.web.js",
"require": "./dist-web/src/index.web.js",
"types": "./dist-web/src/index.web.d.ts"
},
"./native": {
"import": "./dist-native/index.native.js",
"require": "./dist-native/index.native.js",
"types": "./dist-native/index.native.d.ts"
}
},
"devDependencies": {
"@leather.io/eslint-config": "workspace:*",
"@types/node": "20.14.0",
"concurrently": "8.2.2",
"eslint": "8.53.0",
"eslint-config-universe": "12.0.0",
"prettier": "3.3.3",
"tsup": "8.1.0",
"typescript": "5.5.4"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@segment/analytics-next": "1.73.0",
"@segment/analytics-react-native": "2.19.5"
}
}
121 changes: 121 additions & 0 deletions packages/analytics/src/client/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { AnalyticsClientConfig, GenericAnalyticsClient, JsonMap } from '../utils/types.shared';

export type AnalyticsClientOptions = Pick<
AnalyticsClientConfig,
'defaultProperties' | 'defaultTraits'
>;

export class AnalyticsClient<T extends GenericAnalyticsClient> {
constructor(
private readonly client: T,
private readonly options: AnalyticsClientOptions
) {
this.client = client;
this.options = options;
}

private async track(event: string, properties?: JsonMap) {
return this.client.track(event, { ...properties, ...this.options.defaultProperties });
}

public async screen(name: string, properties?: JsonMap) {
return this.client.screen(name, { ...properties, ...this.options.defaultProperties });
}

public async group(groupId: string, traits?: JsonMap) {
return this.client.group(groupId, { ...traits, ...this.options.defaultTraits });
}

public async identify(userId: string, traits?: JsonMap) {
return this.client.identify(userId, { ...traits, ...this.options.defaultTraits });
}

private errorToJsonMap(error: Error) {
return {
errorMessage: error.message,
errorName: error.name,
};
}

// TODO: replace view_transaction_confirmation with this method call.
public async trackViewTransactionConfirmation(properties: { symbol: string }) {
return this.track('Transaction Confirmation Viewed', properties);
}
// TODO: replace ordinals_dot_com_unavailable with this method call.
public async trackOrdinalsDotComUnavailable(properties: { error: Error }) {
return this.track('Ordinals.com Unavailable', this.errorToJsonMap(properties.error));
}
// TODO: replace select_maximum_amount_for_send with this method call.
public async trackSelectMaximumAmountForSend() {
return this.track('Send Amount Selected', {
maximum: true,
});
}
// TODO: replace copy_recipient_bns_address_to_clipboard with this method call.
public async trackCopyToClipboard(properties: { value: 'thing' | 'other-thing' }) {
return this.track('Copy To Clipboard', {
value: properties.value,
});
}
// TODO: replace broadcast_transaction with this method call.
public async trackBroadcastTransaction(properties: { symbol: string }) {
return this.track('Broadcast Transaction', properties);
}
// TODO: replace broadcast_btc_error with this method call.
public async trackBroadcastBtcError(properties: { error: Error; symbol: string }) {
const { error, ...rest } = properties;
return this.track('Broadcast Transaction Error', {
...this.errorToJsonMap(properties.error),
...rest,
});
}
// TODO: replace request_signature_cancel with this method call.
public async trackRequestSignatureCancel() {
return this.track('Request Signature Canceled');
}
// TODO: replace view_transaction_signing with this method call.
public async trackViewTransactionSigning() {
return this.track('Transaction Signing Viewed');
}
// TODO: replace submit_fee_for_transaction with this method call.
public async trackSubmitFeeForTransaction(properties: {
calculation: string;
fee: number | string;
type: string;
}) {
return this.track('Transaction Fee Submitted', properties);
}
// TODO: replace request_update_profile_submit with this method call.
// TODO: replace request_update_profile_cancel with this method call.
public async trackRequestUpdateProfileSubmit(properties: { requestType: 'update' | 'cancel' }) {
return this.track('Update Profile Requested', properties);
}
// TODO: replace non_compliant_entity_detected with this method call.
public async trackNonCompliantEntityDetected(properties?: { address?: string }) {
return this.track('Non Compliant Entity Detected', properties);
}
// TODO: replace requesting_origin_tab_closed_with_pending_action with this method call.
public async trackRequestingOriginTabClosed(properties?: { status: 'action_pending' }) {
return this.track('Origin Tab Closed Requested', properties);
}
// TODO: replace successfully_added_native_segwit_tx_hex_to_ledger_tx with this method call.
// TODO: replace failed_to_add_native_segwit_tx_hex_to_ledger_tx with this method call.
public async trackNativeSegwitTxHexToLedgerTx(properties: { success: boolean }) {
return this.track('Native Segwit Transaction Hex to Ledger', properties);
}
// TODO: replace psbt_sign_request_p2tr_missing_taproot_internal_key with this method call.
public async trackPsbtSignRequest(properties: { status: 'missing_taproot_internal_key' }) {
return this.track('PSBT Sign Request', properties);
}
// TODO: replace ledger_nativesegwit_add_nonwitnessutxo with this method call.
// TODO: replace ledger_nativesegwit_skip_add_nonwitnessutxo with this method call.
public async trackLedgerNativesegwitAddNonwitnessUtxo(properties: {
action: 'add_nonwitness_utxo' | 'skip_add_nonwitness_utxo';
}) {
return this.track('Ledger Native Segwit UTXO Updated', properties);
}
// TODO: replace redux_persist_migration_to_no_serialization with this method call.
public async trackReduxPersistMigrationToNoSerialization() {
return this.track('Redux Persist to No Serialization Migrated');
}
}
29 changes: 29 additions & 0 deletions packages/analytics/src/index.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createClient } from '@segment/analytics-react-native';
import { AnalyticsClientConfig, GenericAnalyticsClient } from 'src/utils/types.shared';

import { AnalyticsClient } from './client/client';

let analyticsClient: AnalyticsClient<GenericAnalyticsClient>;

/**
* Configures the analytics client for the native environment. Must be called before any analytics functions are used.
* @param {AnalyticsClientConfig} config - The configuration for the analytics client.
* @returns {AnalyticsClient<SegmentClient>} The configured analytics client.
*/
const configureAnalyticsClient = ({
writeKey,
defaultProperties,
client,
}: AnalyticsClientConfig) => {
const nativeClient =
client ??
createClient({
writeKey,
});

analyticsClient = new AnalyticsClient(nativeClient, { defaultProperties });

return analyticsClient;
};

export { analyticsClient, configureAnalyticsClient };
25 changes: 25 additions & 0 deletions packages/analytics/src/index.web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { AnalyticsBrowser } from '@segment/analytics-next';
import { AnalyticsClientConfig, GenericAnalyticsClient } from 'src/utils/types.shared';

import { AnalyticsClient } from './client/client';

let analyticsClient: AnalyticsClient<GenericAnalyticsClient>;

/**
* Configures the analytics client for the web environment. Must be called before any analytics functions are used.
* @param {AnalyticsClientConfig} config - The configuration for the analytics client.
* @returns {AnalyticsClient<AnalyticsBrowser>} The configured analytics client.
*/
const configureAnalyticsClient = ({
client,
writeKey,
defaultProperties,
}: AnalyticsClientConfig) => {
const webClient = client ?? AnalyticsBrowser.load({ writeKey });

analyticsClient = new AnalyticsClient(webClient, { defaultProperties });

return analyticsClient;
};

export { analyticsClient, configureAnalyticsClient };
31 changes: 31 additions & 0 deletions packages/analytics/src/utils/types.shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { AnalyticsBrowser } from '@segment/analytics-next/dist/types/browser';
import { SegmentClient } from '@segment/analytics-react-native/lib/typescript/src/analytics';

export interface DefaultProperties {
platform: 'web' | 'extension' | 'mobile';
}

export interface AnalyticsClientConfig {
writeKey: string;
client: GenericAnalyticsClient;
defaultProperties: DefaultProperties;
defaultTraits?: Record<string, JsonValue>;
}

export interface AnalyticsClientInterface {
track: (event: string, properties?: JsonMap) => Promise<void>;
screen: (name: string, properties?: JsonMap) => Promise<void>;
group: (groupId: string, traits?: JsonMap) => Promise<void>;
identify: (userId: string, traits?: JsonMap) => Promise<void>;
}

export type GenericAnalyticsClient = SegmentClient | AnalyticsBrowser | AnalyticsClientInterface;

export type JsonValue = boolean | number | string | null | JsonList | JsonMap | undefined;

export interface JsonMap {
[key: string]: JsonValue;
[index: number]: JsonValue;
}

export type JsonList = JsonValue[];
Loading

0 comments on commit 737d3fb

Please sign in to comment.