Skip to content

Commit

Permalink
Analytics implementation (#136)
Browse files Browse the repository at this point in the history
* Started analytics

* convert track to async function

* Base track custom event

* do not export

* Refactor into a class

* times are strings

* implement id generation

* Work on cacheing

* add tdk versions

* retrying logic

* auto retry submit events

* storage test

* Add README

* Add analytics manager to treasure context

* add uuid types

* fix variable name

* auto set smart account address

* Track custom event does not take smart account address

* move types to own file

* Add example for react

* type fix

* Tracking on login in the react example

* Fix react example

* Added changeset

* README update

* minor fix

* move types to devDeps

* rename xApiKey to apiKey

* make AnalyticsManager a singleton

* Update param and use singleton in react

* fix warning

* fix types

* auto remove events from retrying if older than 7 days
  • Loading branch information
WyattMufson authored Oct 11, 2024
1 parent 6eb6c21 commit 399d230
Show file tree
Hide file tree
Showing 21 changed files with 826 additions and 57 deletions.
6 changes: 6 additions & 0 deletions .changeset/strange-dingos-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@treasure-dev/tdk-react": minor
"@treasure-dev/tdk-core": minor
---

Added support for analytics
1 change: 1 addition & 0 deletions examples/connect-react/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ VITE_TDK_CLIENT_ID=
VITE_TDK_ECOSYSTEM_ID=ecosystem.treasure
VITE_TDK_ECOSYSTEM_PARTNER_ID=
VITE_TDK_BACKEND_WALLET=
VITE_TDK_ANALYTICS_API_KEY=
50 changes: 47 additions & 3 deletions examples/connect-react/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ConnectButton,
useTreasure,
} from "@treasure-dev/tdk-react";
import { useCallback, useState } from "react";
import {
encode,
getContract,
Expand Down Expand Up @@ -33,8 +34,13 @@ const ERC20_MINTABLE_ABI = [
},
] as const;

const cartridgeTag = "tdk-examples-connect-react";

export const App = () => {
const { client, chain, tdk, user, contractAddresses } = useTreasure();
const { client, chain, tdk, user, contractAddresses, trackCustomEvent } =
useTreasure();

const [tracking, setTracking] = useState(false);

const handleMintMagic = async (amount: number) => {
if (!user?.address) {
Expand Down Expand Up @@ -101,6 +107,21 @@ export const App = () => {
}
};

const trackClick = useCallback(async () => {
setTracking(true);
try {
const result = await trackCustomEvent({
cartridgeTag,
name: "test-click",
properties: { test: "test-value" },
});
console.log(`Successfully tracked custom event: ${result}`);
} catch (err) {
console.error("Error tracking custom event:", err);
}
setTracking(false);
}, [trackCustomEvent]);

return (
<div className="mx-auto max-w-5xl space-y-8 p-8">
<header className="flex items-center justify-between gap-3">
Expand All @@ -109,8 +130,23 @@ export const App = () => {
</h1>
<ConnectButton
supportedChainIds={[421614, 42161]}
onConnected={(method, wallet) => {
console.log("Connect successful:", { method, wallet });
onConnected={(method, wallet, nextUser) => {
console.log("Connect successful:", { method, wallet, nextUser });
trackCustomEvent({
address: nextUser?.address,
cartridgeTag,
name: "wallet-connect",
properties: {
method,
walletId: wallet.id,
},
})
.then((result) => {
console.log(`Successfully tracked custom event: ${result}`);
})
.catch((err) => {
console.error("Error tracking custom event:", err);
});
}}
onConnectError={(method, err) => {
console.log("Connect failed:", { method, err });
Expand Down Expand Up @@ -197,6 +233,14 @@ export const App = () => {
</Button>
</div>
</div>
<div className="space-y-1">
<h1 className="font-medium text-xl">Test Analytics</h1>
<div className="flex flex-wrap gap-2">
<Button onClick={trackClick} disabled={tracking}>
{tracking ? "Sending..." : "Track Custom Event"}
</Button>
</div>
</div>
</>
) : (
<p className="text-center">Connect with Treasure to continue</p>
Expand Down
8 changes: 8 additions & 0 deletions examples/connect-react/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ if (root) {
],
nativeTokenLimitPerTransaction: toWei("1"),
}}
analyticsOptions={{
apiKey: import.meta.env.VITE_TDK_ANALYTICS_API_KEY,
appInfo: {
app_identifier: "lol.treasure.examples-react",
app_version: "1.0.0",
app_environment: 0,
},
}}
>
<App />
</TreasureProvider>
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@
"@types/node": "catalog:",
"concurrently": "catalog:",
"husky": "catalog:",
"jsdom": "catalog:",
"knip": "catalog:",
"lint-staged": "catalog:",
"tsup": "catalog:",
"tsx": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vitest": "catalog:"
},
"optionalDependencies": {
Expand Down
4 changes: 4 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,12 @@
"abitype": "catalog:",
"jwt-decode": "catalog:",
"thirdweb": "catalog:",
"uuid": "catalog:",
"viem": "catalog:"
},
"devDependencies": {
"@types/uuid": "catalog:"
},
"peerDependencies": {
"thirdweb": "catalog:"
},
Expand Down
123 changes: 123 additions & 0 deletions packages/core/src/analytics/AnalyticsManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import pjson from "../../package.json";
import { DEFAULT_TDK_DARKMATTER_BASE_URI } from "../constants";
import {
addCachedEvent,
clearCachedEvents,
getCachedEvents,
removeOldEvents,
} from "./storage";
import type { AnalyticsPayload, AppInfo, TrackableEvent } from "./types";
import { getEventId, getServerTime } from "./utils";

export class AnalyticsManager {
static _instance: AnalyticsManager;

initialized = false;

apiUri!: string;

apiKey!: string;

app!: AppInfo;

private constructor() {}

public static get instance(): AnalyticsManager {
if (!AnalyticsManager._instance) {
AnalyticsManager._instance = new AnalyticsManager();
}

return AnalyticsManager._instance;
}

public init({
apiUri = DEFAULT_TDK_DARKMATTER_BASE_URI,
apiKey,
app,
}: { apiUri?: string; apiKey: string; app: AppInfo }) {
this.apiUri = apiUri;
this.apiKey = apiKey;
this.app = app;
this.initialized = true;

setInterval(
() => {
this.retryAllCachedEvents();
},
1000 * 60 * 5,
);
}

/**
* Submits an array of payloads to the Analytics API.
*
* @param {AnalyticsPayload[]} payload - The payloads to submit.
* @returns {Promise<void>} - A promise that resolves when the payloads have been submitted.
*/
async submitPayload(payload: AnalyticsPayload[]): Promise<void> {
if (!this.initialized) {
throw new Error("AnalyticsManager is not initialized");
}
const response = await fetch(`${this.apiUri}/ingress/events`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": this.apiKey,
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Failed to track custom event: ${response.status}`);
}
}

/**
* Tracks a custom event.
*
* @param {TrackableEvent} event - The event to track.
* @param {boolean} cacheOnFailure - Whether to cache the event on failure.
* @returns {Promise<string>} - A promise that resolves with the newly created event's unique ID.
*/
async trackCustomEvent(
event: TrackableEvent,
cacheOnFailure = true,
): Promise<string> {
const serverTime = await getServerTime(this.apiUri);
const localTime = `${Date.now()}`;
const eventId = getEventId();
const payload: AnalyticsPayload = {
...event,
id: eventId,
time_server: serverTime,
time_local: localTime,
app: this.app,
tdk_flavour: "tdk-js",
tdk_version: pjson.version,
};

try {
await this.submitPayload([payload]);
return eventId;
} catch (err) {
console.error("Error tracking custom event:", err);
if (cacheOnFailure) {
addCachedEvent(payload);
}
throw err;
}
}

async retryAllCachedEvents() {
const cachedEvents = getCachedEvents();
if (cachedEvents.length === 0) {
return;
}
try {
await this.submitPayload(cachedEvents);
clearCachedEvents();
} catch (err) {
console.error("Error retrying cached events:", err);
removeOldEvents();
}
}
}
35 changes: 35 additions & 0 deletions packages/core/src/analytics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# TDK Core Analytics

Module for interacting with Treasure’s Data Platform (codename Darkmatter) — a powerful, scalable and real-time streaming analytics service that allows developers to collect data from games.

## Installation

```bash
pnpm add @treasure-dev/tdk-core
```

## Usage

```typescript
import { AnalyticsManager } from "@treasure-dev/tdk-core";

AnalyticsManager.instance.init({
apiUri: "{DARKMATTER_API_BASE_URI}",
xApiKey: "YOUR_X_API_KEY",
app: {
app_identifier: "YOUR_APP_IDENTIFIER",
app_version: "YOUR_APP_VERSION",
app_environment: 0, // 0 for dev, 1 for prod
},
});

// Track a custom event
await AnalyticsManager.instance.trackCustomEvent({
smart_account: "YOUR_SMART_ACCOUNT_ADDRESS", // And/or `user_id`
cartridge_tag: "YOUR_CARTRIDGE_TAG",
name: "YOUR_EVENT_NAME",
properties: {
// Add any additional properties here
},
});
```
62 changes: 62 additions & 0 deletions packages/core/src/analytics/storage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, expect, it } from "vitest";
import {
addCachedEvent,
clearCachedEvents,
getCachedEvents,
removeOldEvents,
} from "./storage";
import type { AnalyticsPayload } from "./types";

describe("analytics storage", () => {
it("adds and gets cached events", () => {
const payload: AnalyticsPayload = {
id: "test-id",
cartridge_tag: "test-cartridge-tag",
name: "test-name",
op: "upsert",
properties: {
test: "test-value",
},
time_server: "test-server-time",
time_local: "test-local-time",
app: {
app_identifier: "test-app-name",
app_version: "test-app-version",
app_environment: 0,
},
smart_account: "test-smart-account",
tdk_flavour: "tdk-js",
tdk_version: "test-tdk-version",
};
addCachedEvent(payload);
expect(getCachedEvents()).toEqual([payload]);
clearCachedEvents();
expect(getCachedEvents()).toEqual([]);
});

it("removes old events", () => {
const payload: AnalyticsPayload = {
id: "test-id",
cartridge_tag: "test-cartridge-tag",
name: "test-name",
op: "upsert",
properties: {
test: "test-value",
},
time_server: `${Date.now() - 1000 * 60 * 60 * 24 * 14}`, // 14 days ago
time_local: `${Date.now() - 1000 * 60 * 60 * 24 * 14}`,
app: {
app_identifier: "test-app-name",
app_version: "test-app-version",
app_environment: 0,
},
smart_account: "test-smart-account",
tdk_flavour: "tdk-js",
tdk_version: "test-tdk-version",
};
addCachedEvent(payload);
expect(getCachedEvents()).toEqual([payload]);
removeOldEvents();
expect(getCachedEvents()).toEqual([]);
});
});
Loading

0 comments on commit 399d230

Please sign in to comment.